From 5e159ff4a93ffd65467007a2b516b8bcabdbe33c Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 28 Oct 2026 09:30:12 -0400 Subject: [PATCH 01/14] Trim down integers We can squeeze another uint32_t into the immediate path and still make the struct smaller by making the immediate and allocated paths a union. This trims down all integer nodes in the tree, and also doubly trims down rationals. --- docs/serialization.md | 4 +- include/prism/integer.h | 54 ++- include/prism/internal/integer.h | 9 + include/prism/internal/parser.h | 4 +- javascript/test.js | 16 + rust/ruby-prism-sys/src/bindings.rs | 80 ++- rust/ruby-prism/src/lib.rs | 31 ++ rust/ruby-prism/src/node.rs | 17 +- src/integer.c | 454 ++++++++---------- src/prism.c | 31 +- src/static_literals.c | 14 +- templates/ext/prism/api_node.c.erb | 6 +- .../org/ruby_lang/prism/Loader.java.erb | 4 +- templates/javascript/src/deserialize.js.erb | 5 +- templates/src/serialize.c.erb | 15 +- 15 files changed, 426 insertions(+), 318 deletions(-) diff --git a/docs/serialization.md b/docs/serialization.md index cbe7746f40..79423b7033 100644 --- a/docs/serialization.md +++ b/docs/serialization.md @@ -85,8 +85,8 @@ Only nodes that own comments have an attachment, and there are none unless the ` | # bytes | field | | --- | --- | | `1` | `1` if the integer is negative, `0` if the integer is positive | -| varuint | the number of words in this integer | -| varuint+ | the words of the integer, least-significant to most-significant | +| varuint | the number of words in this integer, `0` if the integer is zero | +| varuint* | the words of the integer, least-significant to most-significant | ## Structure diff --git a/include/prism/integer.h b/include/prism/integer.h index 9285986885..132dd26c6a 100644 --- a/include/prism/integer.h +++ b/include/prism/integer.h @@ -6,36 +6,62 @@ #ifndef PRISM_INTEGER_H #define PRISM_INTEGER_H +#include "prism/compiler/inline.h" + #include #include #include /** - * A structure represents an arbitrary-sized integer. + * A structure represents an arbitrary-sized integer. The magnitude is stored as + * 32-bit words, least significant first. */ typedef struct { /** - * The number of allocated values. length is set to 0 if the integer fits - * into uint32_t. + * The number of words in the magnitude, with no leading zero words. Zero + * has no words, so a zeroed pm_integer_t is always positive zero. */ - size_t length; + uint32_t length; /** - * List of 32-bit integers. Set to NULL if the integer fits into uint32_t. + * Whether or not the integer is negative. */ - uint32_t *values; + bool negative; /** - * Embedded value for small integer. This value is set to 0 if the value - * does not fit into uint32_t. + * The words of the magnitude. Read them through pm_integer_words rather + * than directly. */ - uint32_t value; + union { + /** + * The words of an integer whose length is at most 2. Words past the + * length are zero. + */ + uint32_t value[2]; - /** - * Whether or not the integer is negative. It is stored this way so that a - * zeroed pm_integer_t is always positive zero. - */ - bool negative; + /** + * The words of an integer whose length is greater than 2. + */ + uint32_t *values; + } words; } pm_integer_t; +/** + * Return the words of an integer, least significant first. + * + * @param integer The integer to read. + * @param length Set to the number of words returned, which is 0 for zero. + * @returns A pointer to the words, valid for as long as the integer is. + */ +static PRISM_INLINE const uint32_t * +pm_integer_words(const pm_integer_t *integer, uint32_t *length) { + *length = integer->length; + + if (integer->length <= 2) { + return integer->words.value; + } else { + return integer->words.values; + } +} + #endif diff --git a/include/prism/internal/integer.h b/include/prism/internal/integer.h index 7c9767e323..cedad6d5af 100644 --- a/include/prism/internal/integer.h +++ b/include/prism/internal/integer.h @@ -9,6 +9,15 @@ #include +/* Return an integer holding the given non-negative value. */ +static PRISM_INLINE pm_integer_t +pm_integer_uint64(uint64_t value) { + return (pm_integer_t) { + .length = value == 0 ? 0 : value <= UINT32_MAX ? 1 : 2, + .words = { .value = { (uint32_t) value, (uint32_t) (value >> 32) } } + }; +} + /* * An enum controlling the base of an integer. It is expected that the base is * already known before parsing the integer, even though it could be derived diff --git a/include/prism/internal/parser.h b/include/prism/internal/parser.h index 7e47127b5b..28d895282e 100644 --- a/include/prism/internal/parser.h +++ b/include/prism/internal/parser.h @@ -812,12 +812,12 @@ struct pm_parser_t { pm_node_flags_t base; /* - * When lexing a decimal integer that fits in a uint32_t, we compute + * When lexing a decimal integer that fits in a uint64_t, we compute * the value during lexing to avoid re-scanning the digits during * parsing. If lexed is true, this holds the result and * pm_integer_parse can be skipped. */ - uint32_t value; + uint64_t value; /* Whether value holds a valid pre-computed integer. */ bool lexed; diff --git a/javascript/test.js b/javascript/test.js index b7e20920a2..8d8c00258c 100644 --- a/javascript/test.js +++ b/javascript/test.js @@ -209,6 +209,22 @@ suite("fields", () => { const result = parse("18446744073709552000"); assert(statement(result).value === 18446744073709552000n); }); + + test("zero", () => { + assert(Object.is(statement(parse("0")).value, 0)); + assert(Object.is(statement(parse("-0")).value, 0)); + }); + + test("word with the high bit set", () => { + assert(statement(parse("2147483648")).value === 2147483648); + assert(statement(parse("4294967295")).value === 4294967295); + assert(statement(parse("-2147483648")).value === -2147483648n); + }); + + test("2 words with the high bit set", () => { + const result = parse("0xFFFFFFFFFFFFFFFF"); + assert(statement(result).value === 18446744073709551615n); + }); }); test("double", () => { diff --git a/rust/ruby-prism-sys/src/bindings.rs b/rust/ruby-prism-sys/src/bindings.rs index a8c2092668..6a96278f65 100644 --- a/rust/ruby-prism-sys/src/bindings.rs +++ b/rust/ruby-prism-sys/src/bindings.rs @@ -2615,26 +2615,20 @@ pub struct pm_constant_id_list_t { pub struct pm_constant_t { _unused: [u8; 0], } -/** A structure represents an arbitrary-sized integer. +/** A structure represents an arbitrary-sized integer. The magnitude is stored as + 32-bit words, least significant first. */ #[repr(C)] -#[derive(Debug, Copy, Clone)] +#[derive(Copy, Clone)] pub struct pm_integer_t { - /** The number of allocated values. length is set to 0 if the integer fits - into uint32_t. -*/ - pub length: usize, - /** List of 32-bit integers. Set to NULL if the integer fits into uint32_t. -*/ - pub values: *mut u32, - /** Embedded value for small integer. This value is set to 0 if the value - does not fit into uint32_t. + /** The number of words in the magnitude, with no leading zero words. Zero + has no words, so a zeroed pm_integer_t is always positive zero. */ - pub value: u32, - /** Whether or not the integer is negative. It is stored this way so that a - zeroed pm_integer_t is always positive zero. + pub length: u32, + /** Whether or not the integer is negative. */ pub negative: bool, + pub words: pm_integer_t__bindgen_ty_1, } /** A view of a sequence of bytes. The string does not own the bytes, so they must remain valid for as long as the string is in use. @@ -6926,7 +6920,7 @@ pub struct pm_instance_variable_write_node { @extends pm_node_t */ #[repr(C)] -#[derive(Debug, Copy, Clone)] +#[derive(Copy, Clone)] pub struct pm_integer_node { /** The embedded base node. */ @@ -8619,7 +8613,7 @@ pub struct pm_range_node { @extends pm_node_t */ #[repr(C)] -#[derive(Debug, Copy, Clone)] +#[derive(Copy, Clone)] pub struct pm_rational_node { /** The embedded base node. */ @@ -10497,6 +10491,20 @@ pub enum pm_comment_type_t { PM_COMMENT_INLINE = 0, PM_COMMENT_EMBDOC = 1, } +/** The words of the magnitude. Read them through pm_integer_words rather + than directly. +*/ +#[repr(C)] +#[derive(Copy, Clone)] +pub union pm_integer_t__bindgen_ty_1 { + /** The words of an integer whose length is at most 2. Words past the + length are zero. +*/ + pub value: [u32; 2usize], + /** The words of an integer whose length is greater than 2. +*/ + pub values: *mut u32, +} impl Default for pm_constant_id_list_t { fn default() -> Self { let mut s = ::std::mem::MaybeUninit::::uninit(); @@ -10506,6 +10514,20 @@ impl Default for pm_constant_id_list_t { } } } +impl Default for pm_integer_t__bindgen_ty_1 { + fn default() -> Self { + let mut s = ::std::mem::MaybeUninit::::uninit(); + unsafe { + ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +impl ::std::fmt::Debug for pm_integer_t__bindgen_ty_1 { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "pm_integer_t__bindgen_ty_1 {{ union }}") + } +} impl Default for pm_integer_t { fn default() -> Self { let mut s = ::std::mem::MaybeUninit::::uninit(); @@ -10515,6 +10537,16 @@ impl Default for pm_integer_t { } } } +impl ::std::fmt::Debug for pm_integer_t { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!( + f, + "pm_integer_t {{ negative: {:?}, words: {:?} }}", + self.negative, + self.words, + ) + } +} impl Default for pm_string_t { fn default() -> Self { let mut s = ::std::mem::MaybeUninit::::uninit(); @@ -11136,6 +11168,11 @@ impl Default for pm_integer_node { } } } +impl ::std::fmt::Debug for pm_integer_node { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "pm_integer_node {{ base: {:?}, value: {:?} }}", self.base, self.value) + } +} impl Default for pm_interpolated_match_last_line_node { fn default() -> Self { let mut s = ::std::mem::MaybeUninit::::uninit(); @@ -11415,6 +11452,17 @@ impl Default for pm_rational_node { } } } +impl ::std::fmt::Debug for pm_rational_node { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!( + f, + "pm_rational_node {{ base: {:?}, numerator: {:?}, denominator: {:?} }}", + self.base, + self.numerator, + self.denominator, + ) + } +} impl Default for pm_regular_expression_node { fn default() -> Self { let mut s = ::std::mem::MaybeUninit::::uninit(); diff --git a/rust/ruby-prism/src/lib.rs b/rust/ruby-prism/src/lib.rs index d33f4a61c7..05c27de768 100644 --- a/rust/ruby-prism/src/lib.rs +++ b/rust/ruby-prism/src/lib.rs @@ -1047,6 +1047,17 @@ end assert_eq!(value, 10); } + #[test] + fn integer_zero_value_test() { + for source in ["0", "-0"] { + let result = parse(source.as_ref()); + let integer = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap().as_integer_node().unwrap().value(); + let value: i32 = integer.try_into().unwrap(); + + assert_eq!(value, 0); + } + } + #[test] fn integer_small_value_to_u32_digits_test() { let result = parse("0xA".as_ref()); @@ -1057,6 +1068,26 @@ end assert_eq!(digits, &[10]); } + #[test] + fn integer_zero_to_u32_digits_test() { + let result = parse("0".as_ref()); + let integer = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap().as_integer_node().unwrap().value(); + let (negative, digits) = integer.to_u32_digits(); + + assert!(!negative); + assert_eq!(digits, &[0]); + } + + #[test] + fn integer_two_digit_value_to_u32_digits_test() { + let result = parse("0xFFFFFFFF00000001".as_ref()); + let integer = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap().as_integer_node().unwrap().value(); + let (negative, digits) = integer.to_u32_digits(); + + assert!(!negative); + assert_eq!(digits, &[1, 4_294_967_295]); + } + #[test] fn integer_large_value_to_u32_digits_test() { let result = parse("0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".as_ref()); diff --git a/rust/ruby-prism/src/node.rs b/rust/ruby-prism/src/node.rs index e797c94b2e..7e9af5b083 100644 --- a/rust/ruby-prism/src/node.rs +++ b/rust/ruby-prism/src/node.rs @@ -269,14 +269,17 @@ impl Integer<'_> { pub const fn to_u32_digits(&self) -> (bool, &[u32]) { let negative = unsafe { (*self.pointer).negative }; let length = unsafe { (*self.pointer).length }; - let values = unsafe { (*self.pointer).values }; - if values.is_null() { - let value_ptr = unsafe { std::ptr::addr_of!((*self.pointer).value) }; - let slice = unsafe { std::slice::from_raw_parts(value_ptr, 1) }; + if length == 0 { + // Zero has no words but is reported as a single 0 digit. + (negative, &[0]) + } else if length <= 2 { + let value_ptr = unsafe { std::ptr::addr_of!((*self.pointer).words.value) }.cast::(); + let slice = unsafe { std::slice::from_raw_parts(value_ptr, length as usize) }; (negative, slice) } else { - let slice = unsafe { std::slice::from_raw_parts(values, length) }; + let values = unsafe { (*self.pointer).words.values }; + let slice = unsafe { std::slice::from_raw_parts(values, length as usize) }; (negative, slice) } } @@ -296,7 +299,9 @@ impl TryInto for Integer<'_> { let length = unsafe { (*self.pointer).length }; if length == 0 { - i32::try_from(unsafe { (*self.pointer).value }).map_or(Err(()), |value| if negative { Ok(-value) } else { Ok(value) }) + Ok(0) + } else if length == 1 { + i32::try_from(unsafe { (*self.pointer).words.value[0] }).map_or(Err(()), |value| if negative { Ok(-value) } else { Ok(value) }) } else { Err(()) } diff --git a/src/integer.c b/src/integer.c index a415f6e3d3..784e6c88ff 100644 --- a/src/integer.c +++ b/src/integer.c @@ -17,249 +17,237 @@ #include /** - * Free the internal memory of an integer. This memory will only be allocated if - * the integer exceeds the size of a single uint32_t. + * An unsigned integer used as scratch space by the arithmetic below. Its words + * are digits in whichever base the operation works in (1<<32 or 10**9), least + * significant first. They always live on the heap and there is always at least + * one, so the arithmetic needs no special cases for small values; + * pm_integer_from_bignum converts a result into a pm_integer_t. */ -static void -pm_integer_free(pm_integer_t *integer) { - if (integer->values) { - xfree(integer->values); - } -} +typedef struct { + /** The number of words. */ + uint32_t length; + + /** The words, least significant first. */ + uint32_t *words; +} pm_bignum_t; /** - * Pull out the length and values from the integer, regardless of the form in - * which the length/values are stored. + * Free the words of a bignum. */ -#define INTEGER_EXTRACT(integer, length_variable, values_variable) \ - if ((integer)->values == NULL) { \ - length_variable = 1; \ - values_variable = &(integer)->value; \ - } else { \ - length_variable = (integer)->length; \ - values_variable = (integer)->values; \ - } +static void +pm_bignum_free(pm_bignum_t *bignum) { + xfree(bignum->words); +} /** - * Adds two positive pm_integer_t with the given base. - * Return pm_integer_t with values allocated. Not normalized. + * Adds two pm_bignum_t with the given base. + * Return pm_bignum_t with words allocated. Not normalized. */ static void -big_add(pm_integer_t *destination, pm_integer_t *left, pm_integer_t *right, uint64_t base) { - size_t left_length; - uint32_t *left_values; - INTEGER_EXTRACT(left, left_length, left_values) +big_add(pm_bignum_t *destination, const pm_bignum_t *left, const pm_bignum_t *right, uint64_t base) { + uint32_t left_length = left->length; + const uint32_t *left_words = left->words; - size_t right_length; - uint32_t *right_values; - INTEGER_EXTRACT(right, right_length, right_values) + uint32_t right_length = right->length; + const uint32_t *right_words = right->words; - size_t length = left_length < right_length ? right_length : left_length; - uint32_t *values = (uint32_t *) xmalloc(sizeof(uint32_t) * (length + 1)); - if (values == NULL) PRISM_OOM(); + uint32_t length = left_length < right_length ? right_length : left_length; + uint32_t *words = (uint32_t *) xmalloc(sizeof(uint32_t) * (length + 1)); + if (words == NULL) PRISM_OOM(); uint64_t carry = 0; for (size_t index = 0; index < length; index++) { - uint64_t sum = carry + (index < left_length ? left_values[index] : 0) + (index < right_length ? right_values[index] : 0); - values[index] = (uint32_t) (sum % base); + uint64_t sum = carry + (index < left_length ? left_words[index] : 0) + (index < right_length ? right_words[index] : 0); + words[index] = (uint32_t) (sum % base); carry = sum / base; } if (carry > 0) { - values[length] = (uint32_t) carry; + words[length] = (uint32_t) carry; length++; } - *destination = (pm_integer_t) { length, values, 0, false }; + *destination = (pm_bignum_t) { length, words }; } /** * Internal use for karatsuba_multiply. Calculates `a - b - c` with the given * base. Assume a, b, c, a - b - c all to be positive. - * Return pm_integer_t with values allocated. Not normalized. + * Return pm_bignum_t with words allocated. Not normalized. */ static void -big_sub2(pm_integer_t *destination, pm_integer_t *a, pm_integer_t *b, pm_integer_t *c, uint64_t base) { - size_t a_length; - uint32_t *a_values; - INTEGER_EXTRACT(a, a_length, a_values) +big_sub2(pm_bignum_t *destination, const pm_bignum_t *a, const pm_bignum_t *b, const pm_bignum_t *c, uint64_t base) { + uint32_t a_length = a->length; + const uint32_t *a_words = a->words; - size_t b_length; - uint32_t *b_values; - INTEGER_EXTRACT(b, b_length, b_values) + uint32_t b_length = b->length; + const uint32_t *b_words = b->words; - size_t c_length; - uint32_t *c_values; - INTEGER_EXTRACT(c, c_length, c_values) + uint32_t c_length = c->length; + const uint32_t *c_words = c->words; - uint32_t *values = (uint32_t*) xmalloc(sizeof(uint32_t) * a_length); - if (values == NULL) PRISM_OOM(); + uint32_t *words = (uint32_t*) xmalloc(sizeof(uint32_t) * a_length); + if (words == NULL) PRISM_OOM(); int64_t carry = 0; for (size_t index = 0; index < a_length; index++) { int64_t sub = ( carry + - a_values[index] - - (index < b_length ? b_values[index] : 0) - - (index < c_length ? c_values[index] : 0) + a_words[index] - + (index < b_length ? b_words[index] : 0) - + (index < c_length ? c_words[index] : 0) ); if (sub >= 0) { - values[index] = (uint32_t) sub; + words[index] = (uint32_t) sub; carry = 0; } else { sub += 2 * (int64_t) base; - values[index] = (uint32_t) ((uint64_t) sub % base); + words[index] = (uint32_t) ((uint64_t) sub % base); carry = sub / (int64_t) base - 2; } } - while (a_length > 1 && values[a_length - 1] == 0) a_length--; - *destination = (pm_integer_t) { a_length, values, 0, false }; + while (a_length > 1 && words[a_length - 1] == 0) a_length--; + *destination = (pm_bignum_t) { a_length, words }; } /** - * Multiply two positive integers with the given base using karatsuba algorithm. - * Return pm_integer_t with values allocated. Not normalized. + * Multiply two pm_bignum_t with the given base using karatsuba algorithm. + * Return pm_bignum_t with words allocated. Not normalized. */ static void -karatsuba_multiply(pm_integer_t *destination, pm_integer_t *left, pm_integer_t *right, uint64_t base) { - size_t left_length; - uint32_t *left_values; - INTEGER_EXTRACT(left, left_length, left_values) +karatsuba_multiply(pm_bignum_t *destination, const pm_bignum_t *left, const pm_bignum_t *right, uint64_t base) { + uint32_t left_length = left->length; + uint32_t *left_words = left->words; - size_t right_length; - uint32_t *right_values; - INTEGER_EXTRACT(right, right_length, right_values) + uint32_t right_length = right->length; + uint32_t *right_words = right->words; if (left_length > right_length) { - size_t temporary_length = left_length; + uint32_t temporary_length = left_length; left_length = right_length; right_length = temporary_length; - uint32_t *temporary_values = left_values; - left_values = right_values; - right_values = temporary_values; + uint32_t *temporary_words = left_words; + left_words = right_words; + right_words = temporary_words; } if (left_length <= 10) { - size_t length = left_length + right_length; - uint32_t *values = (uint32_t *) xcalloc(length, sizeof(uint32_t)); - if (values == NULL) PRISM_OOM(); + uint32_t length = left_length + right_length; + uint32_t *words = (uint32_t *) xcalloc(length, sizeof(uint32_t)); + if (words == NULL) PRISM_OOM(); for (size_t left_index = 0; left_index < left_length; left_index++) { uint32_t carry = 0; for (size_t right_index = 0; right_index < right_length; right_index++) { - uint64_t product = (uint64_t) left_values[left_index] * right_values[right_index] + values[left_index + right_index] + carry; - values[left_index + right_index] = (uint32_t) (product % base); + uint64_t product = (uint64_t) left_words[left_index] * right_words[right_index] + words[left_index + right_index] + carry; + words[left_index + right_index] = (uint32_t) (product % base); carry = (uint32_t) (product / base); } - values[left_index + right_length] = carry; + words[left_index + right_length] = carry; } - while (length > 1 && values[length - 1] == 0) length--; - *destination = (pm_integer_t) { length, values, 0, false }; + while (length > 1 && words[length - 1] == 0) length--; + *destination = (pm_bignum_t) { length, words }; return; } if (left_length * 2 <= right_length) { - uint32_t *values = (uint32_t *) xcalloc(left_length + right_length, sizeof(uint32_t)); - if (values == NULL) PRISM_OOM(); + uint32_t *words = (uint32_t *) xcalloc(left_length + right_length, sizeof(uint32_t)); + if (words == NULL) PRISM_OOM(); - for (size_t start_offset = 0; start_offset < right_length; start_offset += left_length) { - size_t end_offset = start_offset + left_length; + for (uint32_t start_offset = 0; start_offset < right_length; start_offset += left_length) { + uint32_t end_offset = start_offset + left_length; if (end_offset > right_length) end_offset = right_length; - pm_integer_t sliced_left = { + pm_bignum_t sliced_left = { .length = left_length, - .values = left_values, - .value = 0, - .negative = false + .words = left_words }; - pm_integer_t sliced_right = { + pm_bignum_t sliced_right = { .length = end_offset - start_offset, - .values = right_values + start_offset, - .value = 0, - .negative = false + .words = right_words + start_offset }; - pm_integer_t product; + pm_bignum_t product; karatsuba_multiply(&product, &sliced_left, &sliced_right, base); uint32_t carry = 0; for (size_t index = 0; index < product.length; index++) { - uint64_t sum = (uint64_t) values[start_offset + index] + product.values[index] + carry; - values[start_offset + index] = (uint32_t) (sum % base); + uint64_t sum = (uint64_t) words[start_offset + index] + product.words[index] + carry; + words[start_offset + index] = (uint32_t) (sum % base); carry = (uint32_t) (sum / base); } - if (carry > 0) values[start_offset + product.length] += carry; - pm_integer_free(&product); + if (carry > 0) words[start_offset + product.length] += carry; + pm_bignum_free(&product); } - *destination = (pm_integer_t) { left_length + right_length, values, 0, false }; + *destination = (pm_bignum_t) { left_length + right_length, words }; return; } - size_t half = left_length / 2; - pm_integer_t x0 = { half, left_values, 0, false }; - pm_integer_t x1 = { left_length - half, left_values + half, 0, false }; - pm_integer_t y0 = { half, right_values, 0, false }; - pm_integer_t y1 = { right_length - half, right_values + half, 0, false }; + uint32_t half = left_length / 2; + pm_bignum_t x0 = { half, left_words }; + pm_bignum_t x1 = { left_length - half, left_words + half }; + pm_bignum_t y0 = { half, right_words }; + pm_bignum_t y1 = { right_length - half, right_words + half }; - pm_integer_t z0 = { 0 }; + pm_bignum_t z0 = { 0 }; karatsuba_multiply(&z0, &x0, &y0, base); - pm_integer_t z2 = { 0 }; + pm_bignum_t z2 = { 0 }; karatsuba_multiply(&z2, &x1, &y1, base); // For simplicity to avoid considering negative values, // use `z1 = (x0 + x1) * (y0 + y1) - z0 - z2` instead of original karatsuba algorithm. - pm_integer_t x01 = { 0 }; + pm_bignum_t x01 = { 0 }; big_add(&x01, &x0, &x1, base); - pm_integer_t y01 = { 0 }; + pm_bignum_t y01 = { 0 }; big_add(&y01, &y0, &y1, base); - pm_integer_t xy = { 0 }; + pm_bignum_t xy = { 0 }; karatsuba_multiply(&xy, &x01, &y01, base); - pm_integer_t z1; + pm_bignum_t z1; big_sub2(&z1, &xy, &z0, &z2, base); - size_t length = left_length + right_length; - uint32_t *values = (uint32_t*) xcalloc(length, sizeof(uint32_t)); - if (values == NULL) PRISM_OOM(); + uint32_t length = left_length + right_length; + uint32_t *words = (uint32_t*) xcalloc(length, sizeof(uint32_t)); + if (words == NULL) PRISM_OOM(); - assert(z0.values != NULL); - memcpy(values, z0.values, sizeof(uint32_t) * z0.length); + assert(z0.words != NULL); + memcpy(words, z0.words, sizeof(uint32_t) * z0.length); - assert(z2.values != NULL); - memcpy(values + 2 * half, z2.values, sizeof(uint32_t) * z2.length); + assert(z2.words != NULL); + memcpy(words + 2 * half, z2.words, sizeof(uint32_t) * z2.length); uint32_t carry = 0; for(size_t index = 0; index < z1.length; index++) { - uint64_t sum = (uint64_t) carry + values[index + half] + z1.values[index]; - values[index + half] = (uint32_t) (sum % base); + uint64_t sum = (uint64_t) carry + words[index + half] + z1.words[index]; + words[index + half] = (uint32_t) (sum % base); carry = (uint32_t) (sum / base); } for(size_t index = half + z1.length; carry > 0; index++) { - uint64_t sum = (uint64_t) carry + values[index]; - values[index] = (uint32_t) (sum % base); + uint64_t sum = (uint64_t) carry + words[index]; + words[index] = (uint32_t) (sum % base); carry = (uint32_t) (sum / base); } - while (length > 1 && values[length - 1] == 0) length--; - pm_integer_free(&z0); - pm_integer_free(&z1); - pm_integer_free(&z2); - pm_integer_free(&x01); - pm_integer_free(&y01); - pm_integer_free(&xy); + while (length > 1 && words[length - 1] == 0) length--; + pm_bignum_free(&z0); + pm_bignum_free(&z1); + pm_bignum_free(&z2); + pm_bignum_free(&x01); + pm_bignum_free(&y01); + pm_bignum_free(&xy); - *destination = (pm_integer_t) { length, values, 0, false }; + *destination = (pm_bignum_t) { length, words }; } /** @@ -301,125 +289,111 @@ pm_integer_parse_digit(const uint8_t character) { } /** - * Create a pm_integer_t from uint64_t with the given base. It is assumed that - * the memory for the pm_integer_t pointer has been zeroed. + * Create a pm_bignum_t from uint64_t with the given base. */ static void -pm_integer_from_uint64(pm_integer_t *integer, uint64_t value, uint64_t base) { - if (value < base) { - integer->value = (uint32_t) value; - return; - } - - size_t length = 0; +pm_bignum_from_uint64(pm_bignum_t *bignum, uint64_t value, uint64_t base) { + uint32_t length = 0; uint64_t length_value = value; - while (length_value > 0) { + + do { length++; length_value /= base; - } + } while (length_value > 0); - uint32_t *values = (uint32_t *) xmalloc(sizeof(uint32_t) * length); - if (values == NULL) PRISM_OOM(); + uint32_t *words = (uint32_t *) xmalloc(sizeof(uint32_t) * length); + if (words == NULL) PRISM_OOM(); for (size_t value_index = 0; value_index < length; value_index++) { - values[value_index] = (uint32_t) (value % base); + words[value_index] = (uint32_t) (value % base); value /= base; } - integer->length = length; - integer->values = values; + *bignum = (pm_bignum_t) { length, words }; } /** - * Normalize pm_integer_t. - * Heading zero values will be removed. If the integer fits into uint32_t, - * values is set to NULL, length is set to 0, and value field will be used. + * Move a bignum in base 1<<32 into an integer, dropping leading zero words. + * An integer with at most two words stores them inline, so the bignum's words + * are freed; a larger one takes ownership of them. */ static void -pm_integer_normalize(pm_integer_t *integer) { - if (integer->values == NULL) { - return; - } - - while (integer->length > 1 && integer->values[integer->length - 1] == 0) { - integer->length--; - } - - if (integer->length > 1) { - return; +pm_integer_from_bignum(pm_integer_t *integer, pm_bignum_t *bignum) { + uint32_t length = bignum->length; + const uint32_t *words = bignum->words; + while (length > 0 && words[length - 1] == 0) length--; + + if (length <= 2) { + *integer = (pm_integer_t) { + .length = length, + .words = { .value = { length > 0 ? words[0] : 0, length > 1 ? words[1] : 0 } } + }; + pm_bignum_free(bignum); + } else { + *integer = (pm_integer_t) { .length = length, .words = { .values = bignum->words } }; } - - uint32_t value = integer->values[0]; - bool negative = integer->negative && value != 0; - - pm_integer_free(integer); - *integer = (pm_integer_t) { .values = NULL, .value = value, .length = 0, .negative = negative }; } /** - * Convert base of the integer. + * Convert base of the bignum. * In practice, it converts 10**9 to 1<<32 or 1<<32 to 10**9. */ static void -pm_integer_convert_base(pm_integer_t *destination, const pm_integer_t *source, uint64_t base_from, uint64_t base_to) { - size_t source_length; - const uint32_t *source_values; - INTEGER_EXTRACT(source, source_length, source_values) +pm_bignum_convert_base(pm_bignum_t *destination, const pm_bignum_t *source, uint64_t base_from, uint64_t base_to) { + uint32_t source_length = source->length; + const uint32_t *source_words = source->words; size_t bigints_length = (source_length + 1) / 2; assert(bigints_length > 0); - pm_integer_t *bigints = (pm_integer_t *) xcalloc(bigints_length, sizeof(pm_integer_t)); + pm_bignum_t *bigints = (pm_bignum_t *) xcalloc(bigints_length, sizeof(pm_bignum_t)); if (bigints == NULL) PRISM_OOM(); for (size_t index = 0; index < source_length; index += 2) { - uint64_t value = source_values[index] + base_from * (index + 1 < source_length ? source_values[index + 1] : 0); - pm_integer_from_uint64(&bigints[index / 2], value, base_to); + uint64_t value = source_words[index] + base_from * (index + 1 < source_length ? source_words[index + 1] : 0); + pm_bignum_from_uint64(&bigints[index / 2], value, base_to); } - pm_integer_t base = { 0 }; - pm_integer_from_uint64(&base, base_from, base_to); + pm_bignum_t base = { 0 }; + pm_bignum_from_uint64(&base, base_from, base_to); while (bigints_length > 1) { - pm_integer_t next_base; + pm_bignum_t next_base; karatsuba_multiply(&next_base, &base, &base, base_to); - pm_integer_free(&base); + pm_bignum_free(&base); base = next_base; size_t next_length = (bigints_length + 1) / 2; - pm_integer_t *next_bigints = (pm_integer_t *) xcalloc(next_length, sizeof(pm_integer_t)); + pm_bignum_t *next_bigints = (pm_bignum_t *) xcalloc(next_length, sizeof(pm_bignum_t)); if (next_bigints == NULL) PRISM_OOM(); for (size_t bigints_index = 0; bigints_index < bigints_length; bigints_index += 2) { if (bigints_index + 1 == bigints_length) { next_bigints[bigints_index / 2] = bigints[bigints_index]; } else { - pm_integer_t multiplied = { 0 }; + pm_bignum_t multiplied = { 0 }; karatsuba_multiply(&multiplied, &base, &bigints[bigints_index + 1], base_to); big_add(&next_bigints[bigints_index / 2], &bigints[bigints_index], &multiplied, base_to); - pm_integer_free(&bigints[bigints_index]); - pm_integer_free(&bigints[bigints_index + 1]); - pm_integer_free(&multiplied); + pm_bignum_free(&bigints[bigints_index]); + pm_bignum_free(&bigints[bigints_index + 1]); + pm_bignum_free(&multiplied); } } - xfree_sized(bigints, bigints_length * sizeof(pm_integer_t)); + xfree_sized(bigints, bigints_length * sizeof(pm_bignum_t)); bigints = next_bigints; bigints_length = next_length; } *destination = bigints[0]; - destination->negative = source->negative; - pm_integer_normalize(destination); + while (destination->length > 1 && destination->words[destination->length - 1] == 0) destination->length--; - xfree_sized(bigints, bigints_length * sizeof(pm_integer_t)); - pm_integer_free(&base); + xfree_sized(bigints, bigints_length * sizeof(pm_bignum_t)); + pm_bignum_free(&base); } -#undef INTEGER_EXTRACT - /** * Convert digits to integer with the given power-of-two base. */ @@ -428,9 +402,9 @@ pm_integer_parse_powof2(pm_integer_t *integer, uint32_t base, const uint8_t *dig size_t bit = 1; while (base > (uint32_t) (1 << bit)) bit++; - size_t length = (digits_length * bit + 31) / 32; - uint32_t *values = (uint32_t *) xcalloc(length, sizeof(uint32_t)); - if (values == NULL) PRISM_OOM(); + uint32_t length = (uint32_t) ((digits_length * bit + 31) / 32); + uint32_t *words = (uint32_t *) xcalloc(length, sizeof(uint32_t)); + if (words == NULL) PRISM_OOM(); for (size_t digit_index = 0; digit_index < digits_length; digit_index++) { size_t bit_position = bit * (digits_length - digit_index - 1); @@ -439,13 +413,11 @@ pm_integer_parse_powof2(pm_integer_t *integer, uint32_t base, const uint8_t *dig size_t index = bit_position / 32; size_t shift = bit_position % 32; - values[index] |= value << shift; - if (32 - shift < bit) values[index + 1] |= value >> (32 - shift); + words[index] |= value << shift; + if (32 - shift < bit) words[index + 1] |= value >> (32 - shift); } - while (length > 1 && values[length - 1] == 0) length--; - *integer = (pm_integer_t) { .length = length, .values = values, .value = 0, .negative = false }; - pm_integer_normalize(integer); + pm_integer_from_bignum(integer, &((pm_bignum_t) { length, words })); } /** @@ -454,10 +426,10 @@ pm_integer_parse_powof2(pm_integer_t *integer, uint32_t base, const uint8_t *dig static void pm_integer_parse_decimal(pm_integer_t *integer, const uint8_t *digits, size_t digits_length) { const size_t batch = 9; - const size_t length = (digits_length + batch - 1) / batch; + const uint32_t length = (uint32_t) ((digits_length + batch - 1) / batch); - uint32_t *values = (uint32_t *) xcalloc(length, sizeof(uint32_t)); - if (values == NULL) PRISM_OOM(); + uint32_t *words = (uint32_t *) xcalloc(length, sizeof(uint32_t)); + if (words == NULL) PRISM_OOM(); uint32_t value = 0; for (size_t digits_index = 0; digits_index < digits_length; digits_index++) { @@ -465,18 +437,21 @@ pm_integer_parse_decimal(pm_integer_t *integer, const uint8_t *digits, size_t di size_t reverse_index = digits_length - digits_index - 1; if (reverse_index % batch == 0) { - values[reverse_index / batch] = value; + words[reverse_index / batch] = value; value = 0; } } // Convert base from 10**9 to 1<<32. - pm_integer_convert_base(integer, &((pm_integer_t) { .length = length, .values = values, .value = 0, .negative = false }), 1000000000, ((uint64_t) 1 << 32)); - xfree_sized(values, length * sizeof(uint32_t)); + pm_bignum_t converted = { 0 }; + pm_bignum_convert_base(&converted, &((pm_bignum_t) { length, words }), 1000000000, ((uint64_t) 1 << 32)); + xfree_sized(words, length * sizeof(uint32_t)); + + pm_integer_from_bignum(integer, &converted); } /** - * Parse a large integer from a string that does not fit into uint32_t. + * Parse a large integer from a string that does not fit into uint64_t. */ static void pm_integer_parse_big(pm_integer_t *integer, uint32_t multiplier, const uint8_t *start, const uint8_t *end) { @@ -553,22 +528,28 @@ pm_integer_parse(pm_integer_t *integer, pm_integer_base_t base, const uint8_t *s // invalid integer. If this is the case, we'll just return 0. if (start >= end) return; + /* Accumulate into a uint64_t, which covers every integer whose words are + * stored inline. value * multiplier + digit overflows exactly when value + * is past limit, or equal to it with digit past limit_digit. */ + const uint64_t limit = UINT64_MAX / multiplier; + const uint64_t limit_digit = UINT64_MAX % multiplier; + const uint8_t *cursor = start; uint64_t value = (uint64_t) pm_integer_parse_digit(*cursor++); for (; cursor < end; cursor++) { if (*cursor == '_') continue; - value = value * multiplier + (uint64_t) pm_integer_parse_digit(*cursor); + uint64_t digit = (uint64_t) pm_integer_parse_digit(*cursor); - if (value > UINT32_MAX) { - // If the integer is too large to fit into a single uint32_t, then - // we'll parse it as a big integer. + if (value > limit || (value == limit && digit > limit_digit)) { pm_integer_parse_big(integer, multiplier, start, end); return; } + + value = value * multiplier + digit; } - integer->value = (uint32_t) value; + *integer = pm_integer_uint64(value); } /** @@ -581,19 +562,17 @@ pm_integer_compare(const pm_integer_t *left, const pm_integer_t *right) { if (left->negative != right->negative) return left->negative ? -1 : 1; int negative = left->negative ? -1 : 1; - if (left->values == NULL && right->values == NULL) { - if (left->value < right->value) return -1 * negative; - if (left->value > right->value) return 1 * negative; - return 0; - } + if (left->length < right->length) return -1 * negative; + if (left->length > right->length) return 1 * negative; - if (left->values == NULL || left->length < right->length) return -1 * negative; - if (right->values == NULL || left->length > right->length) return 1 * negative; + uint32_t length; + const uint32_t *left_words = pm_integer_words(left, &length); + const uint32_t *right_words = pm_integer_words(right, &length); - for (size_t index = 0; index < left->length; index++) { - size_t value_index = left->length - index - 1; - uint32_t left_value = left->values[value_index]; - uint32_t right_value = right->values[value_index]; + for (uint32_t index = 0; index < length; index++) { + uint32_t value_index = length - index - 1; + uint32_t left_value = left_words[value_index]; + uint32_t right_value = right_words[value_index]; if (left_value < right_value) return -1 * negative; if (left_value > right_value) return 1 * negative; @@ -611,18 +590,18 @@ void pm_integers_reduce(pm_integer_t *numerator, pm_integer_t *denominator) { // even the larger numbers, but for now we're going to keep it simple. if ( // If the numerator doesn't fit into a 32-bit integer, return early. - numerator->length != 0 || + numerator->length > 1 || // If the denominator doesn't fit into a 32-bit integer, return early. - denominator->length != 0 || + denominator->length > 1 || // If the numerator is 0, then return early. - numerator->value == 0 || + numerator->length == 0 || // If the denominator is 1, then return early. - denominator->value == 1 + denominator->words.value[0] == 1 ) return; // Find the greatest common divisor of the numerator and denominator. - uint32_t divisor = numerator->value; - uint32_t remainder = denominator->value; + uint32_t divisor = numerator->words.value[0]; + uint32_t remainder = denominator->words.value[0]; while (remainder != 0) { uint32_t temporary = remainder; @@ -631,8 +610,8 @@ void pm_integers_reduce(pm_integer_t *numerator, pm_integer_t *denominator) { } // Divide the numerator and denominator by the greatest common divisor. - numerator->value /= divisor; - denominator->value /= divisor; + numerator->words.value[0] /= divisor; + denominator->words.value[0] /= divisor; } /** @@ -644,39 +623,26 @@ pm_integer_string(pm_buffer_t *buffer, const pm_integer_t *integer) { pm_buffer_append_byte(buffer, '-'); } - // If the integer fits into a single uint32_t, then we can just append the - // value directly to the buffer. - if (integer->values == NULL) { - pm_buffer_append_format(buffer, "%" PRIu32, integer->value); - return; - } - - // If the integer is two uint32_t values, then we can | them together and - // append the result to the buffer. - if (integer->length == 2) { - const uint64_t value = ((uint64_t) integer->values[0]) | ((uint64_t) integer->values[1] << 32); + /* An integer whose words are inline fits in a uint64_t, so it can be + * appended directly. */ + if (integer->length <= 2) { + const uint64_t value = ((uint64_t) integer->words.value[0]) | ((uint64_t) integer->words.value[1] << 32); pm_buffer_append_format(buffer, "%" PRIu64, value); return; } // Otherwise, first we'll convert the base from 1<<32 to 10**9. - pm_integer_t converted = { 0 }; - pm_integer_convert_base(&converted, integer, (uint64_t) 1 << 32, 1000000000); - - if (converted.values == NULL) { - pm_buffer_append_format(buffer, "%" PRIu32, converted.value); - pm_integer_free(&converted); - return; - } + pm_bignum_t converted = { 0 }; + pm_bignum_convert_base(&converted, &((pm_bignum_t) { integer->length, integer->words.values }), (uint64_t) 1 << 32, 1000000000); // Allocate a buffer that we'll copy the decimal digits into. - const size_t digits_length = converted.length * 9; + const size_t digits_length = (size_t) converted.length * 9; char *digits = xcalloc(digits_length, sizeof(char)); if (digits == NULL) PRISM_OOM(); // Pack bigdecimal to digits. for (size_t value_index = 0; value_index < converted.length; value_index++) { - uint32_t value = converted.values[value_index]; + uint32_t value = converted.words[value_index]; for (size_t digit_index = 0; digit_index < 9; digit_index++) { digits[digits_length - 9 * value_index - digit_index - 1] = (char) ('0' + value % 10); @@ -690,5 +656,5 @@ pm_integer_string(pm_buffer_t *buffer, const pm_integer_t *integer) { // Finally, append the string to the buffer and free the digits. pm_buffer_append_string(buffer, digits + start_offset, digits_length - start_offset); xfree_sized(digits, sizeof(char) * digits_length); - pm_integer_free(&converted); + pm_bignum_free(&converted); } diff --git a/src/prism.c b/src/prism.c index 1ee3d75083..a8b6734a4b 100644 --- a/src/prism.c +++ b/src/prism.c @@ -2283,15 +2283,15 @@ static size_t pm_statements_node_body_length(pm_statements_node_t *node); /** - * Move an integer's values array into the arena. If the integer has heap- - * allocated values, copy them to the arena and free the original. + * Move an integer's words into the arena. If the integer's words are not stored + * inline, copy them to the arena and free the original. */ static PRISM_INLINE void pm_integer_arena_move(pm_arena_t *arena, pm_integer_t *integer) { - if (integer->values != NULL) { + if (integer->length > 2) { size_t byte_size = integer->length * sizeof(uint32_t); - uint32_t *old_values = integer->values; - integer->values = (uint32_t *) pm_arena_memdup(arena, old_values, byte_size, PRISM_ALIGNOF(uint32_t)); + uint32_t *old_values = integer->words.values; + integer->words.values = (uint32_t *) pm_arena_memdup(arena, old_values, byte_size, PRISM_ALIGNOF(uint32_t)); xfree(old_values); } } @@ -4250,7 +4250,7 @@ pm_float_node_rational_create(pm_parser_t *parser, const pm_token_t *token) { size_t length = (size_t) (end - start); if (length == 1) { - node->denominator.value = 1; + node->denominator = pm_integer_uint64(1); return node; } @@ -4773,7 +4773,7 @@ pm_integer_node_create(pm_parser_t *parser, pm_node_flags_t base, const pm_token if (parser->integer.lexed) { // The value was already computed during lexing. - node->value.value = parser->integer.value; + node->value = pm_integer_uint64(parser->integer.value); parser->integer.lexed = false; } else { pm_integer_base_t integer_base = PM_INTEGER_BASE_DECIMAL; @@ -4827,7 +4827,7 @@ pm_integer_node_rational_create(pm_parser_t *parser, pm_node_flags_t base, const base | PM_NODE_FLAG_STATIC_LITERAL, PM_LOCATION_INIT_TOKEN(parser, token), ((pm_integer_t) { 0 }), - ((pm_integer_t) { .value = 1 }) + pm_integer_uint64(1) ); pm_integer_base_t integer_base = PM_INTEGER_BASE_DECIMAL; @@ -8294,18 +8294,18 @@ lex_numeric_prefix(pm_parser_t *parser, bool* seen_e) { const uint8_t *end = parser->end; uint64_t value = (uint64_t) (cursor[-1] - '0'); - bool has_underscore = false; + size_t underscores = 0; bool prev_underscore = false; const uint8_t *invalid = NULL; while (cursor < end) { uint8_t c = *cursor; if (c >= '0' && c <= '9') { - if (value <= UINT32_MAX) value = value * 10 + (uint64_t) (c - '0'); + value = value * 10 + (uint64_t) (c - '0'); prev_underscore = false; cursor++; } else if (c == '_') { - has_underscore = true; + underscores++; if (prev_underscore && invalid == NULL) invalid = cursor; prev_underscore = true; cursor++; @@ -8314,13 +8314,16 @@ lex_numeric_prefix(pm_parser_t *parser, bool* seen_e) { } } - if (has_underscore) { + if (underscores > 0) { if (prev_underscore && invalid == NULL) invalid = cursor - 1; pm_strspn_number_validate(parser, parser->current.end, (size_t) (cursor - parser->current.end), invalid); } - if (value <= UINT32_MAX) { - parser->integer.value = (uint32_t) value; + /* This literal does not start with 0, so at most 19 digits means + * it is below 10**19 and value has not wrapped. */ + size_t digits = (size_t) (cursor - parser->current.end) + 1 - underscores; + if (digits <= 19) { + parser->integer.value = value; parser->integer.lexed = true; } diff --git a/src/static_literals.c b/src/static_literals.c index 9ba092ef08..edac044a8b 100644 --- a/src/static_literals.c +++ b/src/static_literals.c @@ -32,13 +32,15 @@ key_hash(uint64_t type, uint64_t a, uint64_t b) { } /** - * Return the hash of the value of an integer. An integer that fits into a - * uint32_t keys on its value directly, and a larger one keys on the digest of - * its words. + * Return the hash of the value of an integer. An integer whose words are stored + * inline keys on its value directly, and a larger one keys on the digest of its + * words. */ static uint64_t integer_hash(const pm_integer_t *integer) { - uint64_t magnitude = integer->values ? pm_xxh3((const uint8_t *) integer->values, sizeof(uint32_t) * integer->length) : integer->value; + uint64_t magnitude = (integer->length > 2) + ? pm_xxh3((const uint8_t *) integer->words.values, sizeof(uint32_t) * integer->length) + : (((uint64_t) integer->words.value[1] << 32) | integer->words.value[0]); return key_hash(PM_INTEGER_NODE, integer->negative, magnitude); } @@ -121,9 +123,9 @@ int64_value(const pm_parser_t *parser, const pm_node_t *node) { switch (PM_NODE_TYPE(node)) { case PM_INTEGER_NODE: { const pm_integer_t *integer = &((const pm_integer_node_t *) node)->value; - if (integer->values) return integer->negative ? INT64_MIN : INT64_MAX; + if (integer->length > 1) return integer->negative ? INT64_MIN : INT64_MAX; - int64_t value = (int64_t) integer->value; + int64_t value = (int64_t) integer->words.value[0]; return integer->negative ? -value : value; } case PM_SOURCE_LINE_NODE: diff --git a/templates/ext/prism/api_node.c.erb b/templates/ext/prism/api_node.c.erb index 522ca82023..77e43179df 100644 --- a/templates/ext/prism/api_node.c.erb +++ b/templates/ext/prism/api_node.c.erb @@ -49,15 +49,15 @@ pm_string_new(const pm_string_t *string, rb_encoding *encoding) { VALUE pm_integer_new(const pm_integer_t *integer) { VALUE result; - if (integer->values == NULL) { - result = UINT2NUM(integer->value); + if (integer->length <= 2) { + result = ULL2NUM(((unsigned long long) integer->words.value[1] << 32) | integer->words.value[0]); } else { VALUE string = rb_str_new(NULL, integer->length * 8); unsigned char *bytes = (unsigned char *) RSTRING_PTR(string); size_t offset = integer->length * 8; for (size_t value_index = 0; value_index < integer->length; value_index++) { - uint32_t value = integer->values[value_index]; + uint32_t value = integer->words.values[value_index]; for (int index = 0; index < 8; index++) { int byte = (value >> (4 * index)) & 0xf; diff --git a/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Loader.java.erb b/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Loader.java.erb index f360fc21d7..facde6c92e 100644 --- a/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Loader.java.erb +++ b/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Loader.java.erb @@ -283,7 +283,9 @@ public class Loader { boolean negative = buffer.get() != 0; int wordsLength = loadVarUInt(); - assert wordsLength > 0; + if (wordsLength == 0) { + return 0; + } // Load the first word. If it's the only word, then return an int if it // fits into one and a long otherwise. diff --git a/templates/javascript/src/deserialize.js.erb b/templates/javascript/src/deserialize.js.erb index d1fdbf507c..870c5b5a27 100644 --- a/templates/javascript/src/deserialize.js.erb +++ b/templates/javascript/src/deserialize.js.erb @@ -77,7 +77,7 @@ class SerializationBuffer { while (true) { const byte = this.readByte(); - result += (byte & 0x7f) << shift; + result |= (byte & 0x7f) << shift; shift += 7; if ((byte & 0x80) === 0) { @@ -85,7 +85,7 @@ class SerializationBuffer { } } - return result; + return result >>> 0; } readLocation() { @@ -433,6 +433,7 @@ export function deserialize(source, array) { function readInteger() { const negative = buffer.readByte() != 0; const length = buffer.readVarInt(); + if (length == 0) return 0; const firstWord = buffer.readVarInt(); if (length == 1) { diff --git a/templates/src/serialize.c.erb b/templates/src/serialize.c.erb index ca7a57e688..a91d3bda88 100644 --- a/templates/src/serialize.c.erb +++ b/templates/src/serialize.c.erb @@ -62,14 +62,13 @@ pm_serialize_string(const pm_string_t *string, pm_buffer_t *buffer) { static void pm_serialize_integer(const pm_integer_t *integer, pm_buffer_t *buffer) { pm_buffer_append_byte(buffer, integer->negative ? 1 : 0); - if (integer->values == NULL) { - pm_buffer_append_varuint(buffer, pm_sizet_to_u32(1)); - pm_buffer_append_varuint(buffer, integer->value); - } else { - pm_buffer_append_varuint(buffer, pm_sizet_to_u32(integer->length)); - for (size_t i = 0; i < integer->length; i++) { - pm_buffer_append_varuint(buffer, integer->values[i]); - } + + uint32_t length; + const uint32_t *words = pm_integer_words(integer, &length); + + pm_buffer_append_varuint(buffer, length); + for (uint32_t i = 0; i < length; i++) { + pm_buffer_append_varuint(buffer, words[i]); } } From f077d02b15cac77b74fae99c07029039c22481db Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 28 Oct 2026 10:54:55 -0400 Subject: [PATCH 02/14] Allocate pm_integer_t words directly for powof2 --- include/prism/internal/integer.h | 16 ++-- src/integer.c | 127 ++++++++++++++++--------------- src/prism.c | 26 +------ 3 files changed, 73 insertions(+), 96 deletions(-) diff --git a/include/prism/internal/integer.h b/include/prism/internal/integer.h index cedad6d5af..148a0a00e5 100644 --- a/include/prism/internal/integer.h +++ b/include/prism/internal/integer.h @@ -4,6 +4,7 @@ #ifndef PRISM_INTERNAL_INTEGER_H #define PRISM_INTERNAL_INTEGER_H +#include "prism/arena.h" #include "prism/buffer.h" #include "prism/integer.h" @@ -37,23 +38,16 @@ typedef enum { PM_INTEGER_BASE_DECIMAL, /* The hexadecimal base, indicated by a 0x or 0X prefix. */ - PM_INTEGER_BASE_HEXADECIMAL, - - /* - * An unknown base, in which case pm_integer_parse will derive it based on - * the content of the string. This is less efficient and does more - * comparisons, so if callers know the base ahead of time, they should use - * that instead. - */ - PM_INTEGER_BASE_UNKNOWN + PM_INTEGER_BASE_HEXADECIMAL } pm_integer_base_t; /* * Parse an integer from a string. This assumes that the format of the integer * has already been validated, as internal validation checks are not performed - * here. + * here. The words of an integer that does not fit in two words are allocated + * from the given arena. */ -void pm_integer_parse(pm_integer_t *integer, pm_integer_base_t base, const uint8_t *start, const uint8_t *end); +void pm_integer_parse(pm_arena_t *arena, pm_integer_t *integer, pm_integer_base_t base, const uint8_t *start, const uint8_t *end); /* * Compare two integers. This function returns -1 if the left integer is less diff --git a/src/integer.c b/src/integer.c index 784e6c88ff..cb9e52d89c 100644 --- a/src/integer.c +++ b/src/integer.c @@ -1,6 +1,10 @@ #include "prism/internal/integer.h" +#include "prism/compiler/align.h" + #include "prism/internal/allocator.h" +#include "prism/internal/arena.h" +#include "prism/internal/bit.h" #include "prism/internal/buffer.h" #include "prism/internal/oom.h" @@ -395,49 +399,61 @@ pm_bignum_convert_base(pm_bignum_t *destination, const pm_bignum_t *source, uint } /** - * Convert digits to integer with the given power-of-two base. + * Parse the digits of an integer in a power-of-two base that does not fit in + * two words. Every digit but the leading one contributes exactly the same number + * of bits, so the exact number of words is known before parsing, and the digits + * are packed directly into words allocated from the arena. */ static void -pm_integer_parse_powof2(pm_integer_t *integer, uint32_t base, const uint8_t *digits, size_t digits_length) { - size_t bit = 1; - while (base > (uint32_t) (1 << bit)) bit++; +pm_integer_parse_powof2(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, size_t digits, const uint8_t *start, const uint8_t *end) { + static const uint8_t digit_bits[16] = { 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4 }; + size_t leading_bits = digit_bits[pm_integer_parse_digit(*start)]; - uint32_t length = (uint32_t) ((digits_length * bit + 31) / 32); - uint32_t *words = (uint32_t *) xcalloc(length, sizeof(uint32_t)); - if (words == NULL) PRISM_OOM(); + size_t bit = pm_ctzll(base); + uint32_t length = (uint32_t) (((digits - 1) * bit + leading_bits + 31) / 32); + assert(length > 2); + + uint32_t *words = pm_arena_zalloc(arena, sizeof(uint32_t) * length, PRISM_ALIGNOF(uint32_t)); + size_t bit_position = 0; - for (size_t digit_index = 0; digit_index < digits_length; digit_index++) { - size_t bit_position = bit * (digits_length - digit_index - 1); - uint32_t value = digits[digit_index]; + for (const uint8_t *cursor = end; cursor > start;) { + if (*--cursor == '_') continue; + uint32_t value = pm_integer_parse_digit(*cursor); size_t index = bit_position / 32; size_t shift = bit_position % 32; words[index] |= value << shift; - if (32 - shift < bit) words[index + 1] |= value >> (32 - shift); + if (32 - shift < bit && index + 1 < length) words[index + 1] |= value >> (32 - shift); + + bit_position += bit; } - pm_integer_from_bignum(integer, &((pm_bignum_t) { length, words })); + *integer = (pm_integer_t) { .length = length, .words = { .values = words } }; } /** - * Convert decimal digits to pm_integer_t. + * Parse the digits of a decimal integer that does not fit in two words. The + * digits are packed nine at a time into base 10**9 words, which are then + * converted to base 1<<32. */ static void -pm_integer_parse_decimal(pm_integer_t *integer, const uint8_t *digits, size_t digits_length) { +pm_integer_parse_decimal(pm_arena_t *arena, pm_integer_t *integer, size_t digits, const uint8_t *start, const uint8_t *end) { const size_t batch = 9; - const uint32_t length = (uint32_t) ((digits_length + batch - 1) / batch); + const uint32_t length = (uint32_t) ((digits + batch - 1) / batch); uint32_t *words = (uint32_t *) xcalloc(length, sizeof(uint32_t)); if (words == NULL) PRISM_OOM(); uint32_t value = 0; - for (size_t digits_index = 0; digits_index < digits_length; digits_index++) { - value = value * 10 + digits[digits_index]; + size_t remaining = digits; + + for (const uint8_t *cursor = start; cursor < end; cursor++) { + if (*cursor == '_') continue; + value = value * 10 + pm_integer_parse_digit(*cursor); - size_t reverse_index = digits_length - digits_index - 1; - if (reverse_index % batch == 0) { - words[reverse_index / batch] = value; + if (--remaining % batch == 0) { + words[remaining / batch] = value; value = 0; } } @@ -448,32 +464,14 @@ pm_integer_parse_decimal(pm_integer_t *integer, const uint8_t *digits, size_t di xfree_sized(words, length * sizeof(uint32_t)); pm_integer_from_bignum(integer, &converted); -} -/** - * Parse a large integer from a string that does not fit into uint64_t. - */ -static void -pm_integer_parse_big(pm_integer_t *integer, uint32_t multiplier, const uint8_t *start, const uint8_t *end) { - // Allocate an array to store digits. - const size_t digits_capa = sizeof(uint8_t) * (size_t) (end - start); - uint8_t *digits = xmalloc(digits_capa); - if (digits == NULL) PRISM_OOM(); - - size_t digits_length = 0; - for (; start < end; start++) { - if (*start == '_') continue; - digits[digits_length++] = pm_integer_parse_digit(*start); + /* The base conversion leaves the words on the heap, so copy them into the + * arena. */ + if (integer->length > 2) { + uint32_t *heap_words = integer->words.values; + integer->words.values = (uint32_t *) pm_arena_memdup(arena, heap_words, sizeof(uint32_t) * integer->length, PRISM_ALIGNOF(uint32_t)); + xfree(heap_words); } - - // Construct pm_integer_t from the digits. - if (multiplier == 10) { - pm_integer_parse_decimal(integer, digits, digits_length); - } else { - pm_integer_parse_powof2(integer, multiplier, digits, digits_length); - } - - xfree_sized(digits, digits_capa); } /** @@ -482,7 +480,7 @@ pm_integer_parse_big(pm_integer_t *integer, uint32_t multiplier, const uint8_t * * here. */ void -pm_integer_parse(pm_integer_t *integer, pm_integer_base_t base, const uint8_t *start, const uint8_t *end) { +pm_integer_parse(pm_arena_t *arena, pm_integer_t *integer, pm_integer_base_t base, const uint8_t *start, const uint8_t *end) { // Ignore unary +. Unary - is parsed differently and will not end up here. // Instead, it will modify the parsed integer later. if (*start == '+') start++; @@ -491,7 +489,6 @@ pm_integer_parse(pm_integer_t *integer, pm_integer_base_t base, const uint8_t *s uint32_t multiplier = 10; switch (base) { case PM_INTEGER_BASE_DEFAULT: - while (start < end && *start == '0') start++; // 01 -> 1 break; case PM_INTEGER_BASE_BINARY: start += 2; // 0b @@ -509,24 +506,18 @@ pm_integer_parse(pm_integer_t *integer, pm_integer_base_t base, const uint8_t *s start += 2; // 0x multiplier = 16; break; - case PM_INTEGER_BASE_UNKNOWN: - if (*start == '0' && (end - start) > 1) { - switch (start[1]) { - case '_': start += 2; multiplier = 8; break; - case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': start++; multiplier = 8; break; - case 'b': case 'B': start += 2; multiplier = 2; break; - case 'o': case 'O': start += 2; multiplier = 8; break; - case 'd': case 'D': start += 2; break; - case 'x': case 'X': start += 2; multiplier = 16; break; - default: assert(false && "unreachable"); break; - } - } - break; } - // It's possible that we've consumed everything at this point if there is an - // invalid integer. If this is the case, we'll just return 0. - if (start >= end) return; + /* Leading zeros do not change the value, so skip them here and the paths + * below always start at a nonzero digit. */ + while (start < end && (*start == '0' || *start == '_')) start++; + + /* Everything has been consumed if the integer is zero, or if it is invalid + * and has no digits. Either way the result is zero. */ + if (start >= end) { + *integer = (pm_integer_t) { 0 }; + return; + } /* Accumulate into a uint64_t, which covers every integer whose words are * stored inline. value * multiplier + digit overflows exactly when value @@ -542,7 +533,17 @@ pm_integer_parse(pm_integer_t *integer, pm_integer_base_t base, const uint8_t *s uint64_t digit = (uint64_t) pm_integer_parse_digit(*cursor); if (value > limit || (value == limit && digit > limit_digit)) { - pm_integer_parse_big(integer, multiplier, start, end); + size_t digits = 0; + for (const uint8_t *scan = start; scan < end; scan++) { + if (*scan != '_') digits++; + } + + if (multiplier == 10) { + pm_integer_parse_decimal(arena, integer, digits, start, end); + } else { + pm_integer_parse_powof2(arena, integer, multiplier, digits, start, end); + } + return; } diff --git a/src/prism.c b/src/prism.c index a8b6734a4b..3d7bebd50b 100644 --- a/src/prism.c +++ b/src/prism.c @@ -2282,20 +2282,6 @@ pm_statements_node_body_append(pm_parser_t *parser, pm_statements_node_t *node, static size_t pm_statements_node_body_length(pm_statements_node_t *node); -/** - * Move an integer's words into the arena. If the integer's words are not stored - * inline, copy them to the arena and free the original. - */ -static PRISM_INLINE void -pm_integer_arena_move(pm_arena_t *arena, pm_integer_t *integer) { - if (integer->length > 2) { - size_t byte_size = integer->length * sizeof(uint32_t); - uint32_t *old_values = integer->words.values; - integer->words.values = (uint32_t *) pm_arena_memdup(arena, old_values, byte_size, PRISM_ALIGNOF(uint32_t)); - xfree(old_values); - } -} - /** * Allocate a new ErrorRecoveryNode node with no unexpected child. */ @@ -4262,7 +4248,7 @@ pm_float_node_rational_create(pm_parser_t *parser, const pm_token_t *token) { memcpy(digits, start, (unsigned long) (point - start)); memcpy(digits + (point - start), point + 1, (unsigned long) (end - point - 1)); - pm_integer_parse(&node->numerator, PM_INTEGER_BASE_DEFAULT, digits, digits + length - 1); + pm_integer_parse(parser->arena, &node->numerator, PM_INTEGER_BASE_DEFAULT, digits, digits + length - 1); size_t fract_length = 0; for (const uint8_t *fract = point; fract < end; ++fract) { @@ -4270,12 +4256,10 @@ pm_float_node_rational_create(pm_parser_t *parser, const pm_token_t *token) { } digits[0] = '1'; if (fract_length > 1) memset(digits + 1, '0', fract_length - 1); - pm_integer_parse(&node->denominator, PM_INTEGER_BASE_DEFAULT, digits, digits + fract_length); + pm_integer_parse(parser->arena, &node->denominator, PM_INTEGER_BASE_DEFAULT, digits, digits + fract_length); xfree_sized(digits, length); pm_integers_reduce(&node->numerator, &node->denominator); - pm_integer_arena_move(parser->arena, &node->numerator); - pm_integer_arena_move(parser->arena, &node->denominator); return node; } @@ -4785,8 +4769,7 @@ pm_integer_node_create(pm_parser_t *parser, pm_node_flags_t base, const pm_token default: assert(false && "unreachable"); break; } - pm_integer_parse(&node->value, integer_base, token->start, token->end); - pm_integer_arena_move(parser->arena, &node->value); + pm_integer_parse(parser->arena, &node->value, integer_base, token->start, token->end); } return node; @@ -4839,8 +4822,7 @@ pm_integer_node_rational_create(pm_parser_t *parser, pm_node_flags_t base, const default: assert(false && "unreachable"); break; } - pm_integer_parse(&node->numerator, integer_base, token->start, token->end - 1); - pm_integer_arena_move(parser->arena, &node->numerator); + pm_integer_parse(parser->arena, &node->numerator, integer_base, token->start, token->end - 1); return node; } From b6298c3db1e98a420c129e84ec5c90c3d62f67be Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 28 Oct 2026 12:02:56 -0400 Subject: [PATCH 03/14] Make bignum math inline to avoid allocations --- include/prism/internal/arena.h | 13 + include/prism/internal/bignum.h | 83 +++++ prism.gemspec | 1 + src/integer.c | 545 +++++++------------------------- 4 files changed, 203 insertions(+), 439 deletions(-) create mode 100644 include/prism/internal/bignum.h diff --git a/include/prism/internal/arena.h b/include/prism/internal/arena.h index 2e413b42bf..3327654382 100644 --- a/include/prism/internal/arena.h +++ b/include/prism/internal/arena.h @@ -8,6 +8,7 @@ #include "prism/arena.h" +#include #include #include @@ -105,4 +106,16 @@ pm_arena_memdup(pm_arena_t *arena, const void *src, size_t size, size_t alignmen return dst; } +/* + * Shrink the most recent allocation from the arena to new_size bytes, giving + * the rest back to the arena. The allocation must be the most recent one, so + * that it ends exactly where the current block's used bytes do. + */ +static PRISM_INLINE void +pm_arena_shrink(pm_arena_t *arena, void *ptr, size_t size, size_t new_size) { + assert(new_size <= size); + assert(arena->current != NULL && (char *) ptr + size == arena->current->data + arena->current->used); + arena->current->used -= size - new_size; +} + #endif diff --git a/include/prism/internal/bignum.h b/include/prism/internal/bignum.h new file mode 100644 index 0000000000..e8a0a5321a --- /dev/null +++ b/include/prism/internal/bignum.h @@ -0,0 +1,83 @@ +/* + * Arithmetic on arbitrary-precision unsigned integers stored as arrays of + * 32-bit words. + */ +#ifndef PRISM_INTERNAL_BIGNUM_H +#define PRISM_INTERNAL_BIGNUM_H + +#include "prism/compiler/inline.h" + +#include +#include + +/* + * An arbitrary-precision unsigned integer. + */ +typedef struct { + /* The number of words in use. */ + uint32_t length; + + /* The words, least significant first. */ + uint32_t *words; +} pm_bignum_t; + +/* + * Multiply the number by multiplier and add addend, in place. A word times a + * word plus a word is at most 2**64 - 2**32, so each intermediate product fits + * in a uint64_t. A nonzero carry out of the top word becomes a new top word, so + * the storage must have room for it. + */ +static PRISM_INLINE void +pm_bignum_mul_add(pm_bignum_t *bignum, uint32_t multiplier, uint32_t addend) { + assert(multiplier != 0); + uint64_t carry = addend; + + for (uint32_t index = 0; index < bignum->length; index++) { + uint64_t product = (uint64_t) bignum->words[index] * multiplier + carry; + bignum->words[index] = (uint32_t) product; + carry = product >> 32; + } + + if (carry != 0) bignum->words[bignum->length++] = (uint32_t) carry; +} + +/* + * Divide the number by divisor, in place, working down from the top word. + * Returns the remainder. The remainder is always below the divisor, so shifting + * it above the next word fits in a uint64_t, and since the divisor is a single + * word the quotient is at most one word shorter. When inlined with a constant + * divisor, the compiler replaces the division with a multiplication. + */ +static PRISM_INLINE uint32_t +pm_bignum_div(pm_bignum_t *bignum, uint32_t divisor) { + assert(divisor != 0); + uint64_t remainder = 0; + + for (uint32_t index = bignum->length; index > 0; index--) { + uint64_t current = (remainder << 32) | bignum->words[index - 1]; + bignum->words[index - 1] = (uint32_t) (current / divisor); + remainder = current % divisor; + } + + if (bignum->length > 0 && bignum->words[bignum->length - 1] == 0) bignum->length--; + return (uint32_t) remainder; +} + +/* + * Compare two numbers. Returns -1 if left is less than right, 0 if they are + * equal, and 1 if left is greater than right. + */ +static PRISM_INLINE int +pm_bignum_compare(const pm_bignum_t *left, const pm_bignum_t *right) { + if (left->length != right->length) return left->length < right->length ? -1 : 1; + + for (uint32_t index = left->length; index > 0; index--) { + uint32_t left_word = left->words[index - 1]; + uint32_t right_word = right->words[index - 1]; + if (left_word != right_word) return left_word < right_word ? -1 : 1; + } + + return 0; +} + +#endif diff --git a/prism.gemspec b/prism.gemspec index b63078626f..7af363b586 100644 --- a/prism.gemspec +++ b/prism.gemspec @@ -69,6 +69,7 @@ Gem::Specification.new do |spec| "include/prism/internal/allocator.h", "include/prism/internal/allocator_debug.h", "include/prism/internal/arena.h", + "include/prism/internal/bignum.h", "include/prism/internal/bit.h", "include/prism/internal/buffer.h", "include/prism/internal/char.h", diff --git a/src/integer.c b/src/integer.c index cb9e52d89c..a6d9f31389 100644 --- a/src/integer.c +++ b/src/integer.c @@ -4,6 +4,7 @@ #include "prism/internal/allocator.h" #include "prism/internal/arena.h" +#include "prism/internal/bignum.h" #include "prism/internal/bit.h" #include "prism/internal/buffer.h" #include "prism/internal/oom.h" @@ -20,240 +21,6 @@ #include #include -/** - * An unsigned integer used as scratch space by the arithmetic below. Its words - * are digits in whichever base the operation works in (1<<32 or 10**9), least - * significant first. They always live on the heap and there is always at least - * one, so the arithmetic needs no special cases for small values; - * pm_integer_from_bignum converts a result into a pm_integer_t. - */ -typedef struct { - /** The number of words. */ - uint32_t length; - - /** The words, least significant first. */ - uint32_t *words; -} pm_bignum_t; - -/** - * Free the words of a bignum. - */ -static void -pm_bignum_free(pm_bignum_t *bignum) { - xfree(bignum->words); -} - -/** - * Adds two pm_bignum_t with the given base. - * Return pm_bignum_t with words allocated. Not normalized. - */ -static void -big_add(pm_bignum_t *destination, const pm_bignum_t *left, const pm_bignum_t *right, uint64_t base) { - uint32_t left_length = left->length; - const uint32_t *left_words = left->words; - - uint32_t right_length = right->length; - const uint32_t *right_words = right->words; - - uint32_t length = left_length < right_length ? right_length : left_length; - uint32_t *words = (uint32_t *) xmalloc(sizeof(uint32_t) * (length + 1)); - if (words == NULL) PRISM_OOM(); - - uint64_t carry = 0; - for (size_t index = 0; index < length; index++) { - uint64_t sum = carry + (index < left_length ? left_words[index] : 0) + (index < right_length ? right_words[index] : 0); - words[index] = (uint32_t) (sum % base); - carry = sum / base; - } - - if (carry > 0) { - words[length] = (uint32_t) carry; - length++; - } - - *destination = (pm_bignum_t) { length, words }; -} - -/** - * Internal use for karatsuba_multiply. Calculates `a - b - c` with the given - * base. Assume a, b, c, a - b - c all to be positive. - * Return pm_bignum_t with words allocated. Not normalized. - */ -static void -big_sub2(pm_bignum_t *destination, const pm_bignum_t *a, const pm_bignum_t *b, const pm_bignum_t *c, uint64_t base) { - uint32_t a_length = a->length; - const uint32_t *a_words = a->words; - - uint32_t b_length = b->length; - const uint32_t *b_words = b->words; - - uint32_t c_length = c->length; - const uint32_t *c_words = c->words; - - uint32_t *words = (uint32_t*) xmalloc(sizeof(uint32_t) * a_length); - if (words == NULL) PRISM_OOM(); - - int64_t carry = 0; - for (size_t index = 0; index < a_length; index++) { - int64_t sub = ( - carry + - a_words[index] - - (index < b_length ? b_words[index] : 0) - - (index < c_length ? c_words[index] : 0) - ); - - if (sub >= 0) { - words[index] = (uint32_t) sub; - carry = 0; - } else { - sub += 2 * (int64_t) base; - words[index] = (uint32_t) ((uint64_t) sub % base); - carry = sub / (int64_t) base - 2; - } - } - - while (a_length > 1 && words[a_length - 1] == 0) a_length--; - *destination = (pm_bignum_t) { a_length, words }; -} - -/** - * Multiply two pm_bignum_t with the given base using karatsuba algorithm. - * Return pm_bignum_t with words allocated. Not normalized. - */ -static void -karatsuba_multiply(pm_bignum_t *destination, const pm_bignum_t *left, const pm_bignum_t *right, uint64_t base) { - uint32_t left_length = left->length; - uint32_t *left_words = left->words; - - uint32_t right_length = right->length; - uint32_t *right_words = right->words; - - if (left_length > right_length) { - uint32_t temporary_length = left_length; - left_length = right_length; - right_length = temporary_length; - - uint32_t *temporary_words = left_words; - left_words = right_words; - right_words = temporary_words; - } - - if (left_length <= 10) { - uint32_t length = left_length + right_length; - uint32_t *words = (uint32_t *) xcalloc(length, sizeof(uint32_t)); - if (words == NULL) PRISM_OOM(); - - for (size_t left_index = 0; left_index < left_length; left_index++) { - uint32_t carry = 0; - for (size_t right_index = 0; right_index < right_length; right_index++) { - uint64_t product = (uint64_t) left_words[left_index] * right_words[right_index] + words[left_index + right_index] + carry; - words[left_index + right_index] = (uint32_t) (product % base); - carry = (uint32_t) (product / base); - } - words[left_index + right_length] = carry; - } - - while (length > 1 && words[length - 1] == 0) length--; - *destination = (pm_bignum_t) { length, words }; - return; - } - - if (left_length * 2 <= right_length) { - uint32_t *words = (uint32_t *) xcalloc(left_length + right_length, sizeof(uint32_t)); - if (words == NULL) PRISM_OOM(); - - for (uint32_t start_offset = 0; start_offset < right_length; start_offset += left_length) { - uint32_t end_offset = start_offset + left_length; - if (end_offset > right_length) end_offset = right_length; - - pm_bignum_t sliced_left = { - .length = left_length, - .words = left_words - }; - - pm_bignum_t sliced_right = { - .length = end_offset - start_offset, - .words = right_words + start_offset - }; - - pm_bignum_t product; - karatsuba_multiply(&product, &sliced_left, &sliced_right, base); - - uint32_t carry = 0; - for (size_t index = 0; index < product.length; index++) { - uint64_t sum = (uint64_t) words[start_offset + index] + product.words[index] + carry; - words[start_offset + index] = (uint32_t) (sum % base); - carry = (uint32_t) (sum / base); - } - - if (carry > 0) words[start_offset + product.length] += carry; - pm_bignum_free(&product); - } - - *destination = (pm_bignum_t) { left_length + right_length, words }; - return; - } - - uint32_t half = left_length / 2; - pm_bignum_t x0 = { half, left_words }; - pm_bignum_t x1 = { left_length - half, left_words + half }; - pm_bignum_t y0 = { half, right_words }; - pm_bignum_t y1 = { right_length - half, right_words + half }; - - pm_bignum_t z0 = { 0 }; - karatsuba_multiply(&z0, &x0, &y0, base); - - pm_bignum_t z2 = { 0 }; - karatsuba_multiply(&z2, &x1, &y1, base); - - // For simplicity to avoid considering negative values, - // use `z1 = (x0 + x1) * (y0 + y1) - z0 - z2` instead of original karatsuba algorithm. - pm_bignum_t x01 = { 0 }; - big_add(&x01, &x0, &x1, base); - - pm_bignum_t y01 = { 0 }; - big_add(&y01, &y0, &y1, base); - - pm_bignum_t xy = { 0 }; - karatsuba_multiply(&xy, &x01, &y01, base); - - pm_bignum_t z1; - big_sub2(&z1, &xy, &z0, &z2, base); - - uint32_t length = left_length + right_length; - uint32_t *words = (uint32_t*) xcalloc(length, sizeof(uint32_t)); - if (words == NULL) PRISM_OOM(); - - assert(z0.words != NULL); - memcpy(words, z0.words, sizeof(uint32_t) * z0.length); - - assert(z2.words != NULL); - memcpy(words + 2 * half, z2.words, sizeof(uint32_t) * z2.length); - - uint32_t carry = 0; - for(size_t index = 0; index < z1.length; index++) { - uint64_t sum = (uint64_t) carry + words[index + half] + z1.words[index]; - words[index + half] = (uint32_t) (sum % base); - carry = (uint32_t) (sum / base); - } - - for(size_t index = half + z1.length; carry > 0; index++) { - uint64_t sum = (uint64_t) carry + words[index]; - words[index] = (uint32_t) (sum % base); - carry = (uint32_t) (sum / base); - } - - while (length > 1 && words[length - 1] == 0) length--; - pm_bignum_free(&z0); - pm_bignum_free(&z1); - pm_bignum_free(&z2); - pm_bignum_free(&x01); - pm_bignum_free(&y01); - pm_bignum_free(&xy); - - *destination = (pm_bignum_t) { length, words }; -} - /** * The values of a hexadecimal digit, where the index is the ASCII character. * Note that there's an odd exception here where _ is mapped to 0. This is @@ -262,23 +29,23 @@ karatsuba_multiply(pm_bignum_t *destination, const pm_bignum_t *left, const pm_b * the user. */ static const int8_t pm_integer_parse_digit_values[256] = { -// 0 1 2 3 4 5 6 7 8 9 A B C D E F - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 1x - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 2x - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, // 3x - -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 4x - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, // 5x - -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 6x - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 7x - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 8x - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 9x - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Ax - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Bx - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Cx - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Dx - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Ex - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Fx +/* 0 1 2 3 4 5 6 7 8 9 A B C D E F */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 1x */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 2x */ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, /* 3x */ + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 4x */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, /* 5x */ + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 6x */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 7x */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 8x */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 9x */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* Ax */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* Bx */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* Cx */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* Dx */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* Ex */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* Fx */ }; /** @@ -292,112 +59,6 @@ pm_integer_parse_digit(const uint8_t character) { return (uint8_t) value; } -/** - * Create a pm_bignum_t from uint64_t with the given base. - */ -static void -pm_bignum_from_uint64(pm_bignum_t *bignum, uint64_t value, uint64_t base) { - uint32_t length = 0; - uint64_t length_value = value; - - do { - length++; - length_value /= base; - } while (length_value > 0); - - uint32_t *words = (uint32_t *) xmalloc(sizeof(uint32_t) * length); - if (words == NULL) PRISM_OOM(); - - for (size_t value_index = 0; value_index < length; value_index++) { - words[value_index] = (uint32_t) (value % base); - value /= base; - } - - *bignum = (pm_bignum_t) { length, words }; -} - -/** - * Move a bignum in base 1<<32 into an integer, dropping leading zero words. - * An integer with at most two words stores them inline, so the bignum's words - * are freed; a larger one takes ownership of them. - */ -static void -pm_integer_from_bignum(pm_integer_t *integer, pm_bignum_t *bignum) { - uint32_t length = bignum->length; - const uint32_t *words = bignum->words; - while (length > 0 && words[length - 1] == 0) length--; - - if (length <= 2) { - *integer = (pm_integer_t) { - .length = length, - .words = { .value = { length > 0 ? words[0] : 0, length > 1 ? words[1] : 0 } } - }; - pm_bignum_free(bignum); - } else { - *integer = (pm_integer_t) { .length = length, .words = { .values = bignum->words } }; - } -} - -/** - * Convert base of the bignum. - * In practice, it converts 10**9 to 1<<32 or 1<<32 to 10**9. - */ -static void -pm_bignum_convert_base(pm_bignum_t *destination, const pm_bignum_t *source, uint64_t base_from, uint64_t base_to) { - uint32_t source_length = source->length; - const uint32_t *source_words = source->words; - - size_t bigints_length = (source_length + 1) / 2; - assert(bigints_length > 0); - - pm_bignum_t *bigints = (pm_bignum_t *) xcalloc(bigints_length, sizeof(pm_bignum_t)); - if (bigints == NULL) PRISM_OOM(); - - for (size_t index = 0; index < source_length; index += 2) { - uint64_t value = source_words[index] + base_from * (index + 1 < source_length ? source_words[index + 1] : 0); - pm_bignum_from_uint64(&bigints[index / 2], value, base_to); - } - - pm_bignum_t base = { 0 }; - pm_bignum_from_uint64(&base, base_from, base_to); - - while (bigints_length > 1) { - pm_bignum_t next_base; - karatsuba_multiply(&next_base, &base, &base, base_to); - - pm_bignum_free(&base); - base = next_base; - - size_t next_length = (bigints_length + 1) / 2; - pm_bignum_t *next_bigints = (pm_bignum_t *) xcalloc(next_length, sizeof(pm_bignum_t)); - if (next_bigints == NULL) PRISM_OOM(); - - for (size_t bigints_index = 0; bigints_index < bigints_length; bigints_index += 2) { - if (bigints_index + 1 == bigints_length) { - next_bigints[bigints_index / 2] = bigints[bigints_index]; - } else { - pm_bignum_t multiplied = { 0 }; - karatsuba_multiply(&multiplied, &base, &bigints[bigints_index + 1], base_to); - - big_add(&next_bigints[bigints_index / 2], &bigints[bigints_index], &multiplied, base_to); - pm_bignum_free(&bigints[bigints_index]); - pm_bignum_free(&bigints[bigints_index + 1]); - pm_bignum_free(&multiplied); - } - } - - xfree_sized(bigints, bigints_length * sizeof(pm_bignum_t)); - bigints = next_bigints; - bigints_length = next_length; - } - - *destination = bigints[0]; - while (destination->length > 1 && destination->words[destination->length - 1] == 0) destination->length--; - - xfree_sized(bigints, bigints_length * sizeof(pm_bignum_t)); - pm_bignum_free(&base); -} - /** * Parse the digits of an integer in a power-of-two base that does not fit in * two words. Every digit but the leading one contributes exactly the same number @@ -434,44 +95,45 @@ pm_integer_parse_powof2(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, /** * Parse the digits of a decimal integer that does not fit in two words. The - * digits are packed nine at a time into base 10**9 words, which are then - * converted to base 1<<32. + * words are built in place in the arena: starting from zero, the number is + * multiplied by 10**9 and the next nine digits are added, until every digit is + * consumed. The first chunk takes the leftover digits so the rest are exactly + * nine. */ static void pm_integer_parse_decimal(pm_arena_t *arena, pm_integer_t *integer, size_t digits, const uint8_t *start, const uint8_t *end) { - const size_t batch = 9; - const uint32_t length = (uint32_t) ((digits + batch - 1) / batch); - - uint32_t *words = (uint32_t *) xcalloc(length, sizeof(uint32_t)); - if (words == NULL) PRISM_OOM(); - - uint32_t value = 0; - size_t remaining = digits; + static const uint32_t powof10[10] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 }; + + /* A number with d digits is below 10**d, so it needs at most + * d * log2(10) + 1 bits. 3402/1024 is just above log2(10), so this never + * undercounts, and the words that turn out to be unneeded are given back to + * the arena below. */ + size_t capacity = (digits * 3402 / 1024 + 32) / 32; + pm_bignum_t bignum = { + .length = 0, + .words = (uint32_t *) pm_arena_alloc(arena, sizeof(uint32_t) * capacity, PRISM_ALIGNOF(uint32_t)) + }; + + size_t chunk_size = digits % 9 == 0 ? 9 : digits % 9; + size_t chunk_digits = 0; + uint32_t chunk = 0; for (const uint8_t *cursor = start; cursor < end; cursor++) { if (*cursor == '_') continue; - value = value * 10 + pm_integer_parse_digit(*cursor); + chunk = chunk * 10 + pm_integer_parse_digit(*cursor); + + if (++chunk_digits == chunk_size) { + pm_bignum_mul_add(&bignum, powof10[chunk_size], chunk); - if (--remaining % batch == 0) { - words[remaining / batch] = value; - value = 0; + chunk_size = 9; + chunk_digits = 0; + chunk = 0; } } - // Convert base from 10**9 to 1<<32. - pm_bignum_t converted = { 0 }; - pm_bignum_convert_base(&converted, &((pm_bignum_t) { length, words }), 1000000000, ((uint64_t) 1 << 32)); - xfree_sized(words, length * sizeof(uint32_t)); - - pm_integer_from_bignum(integer, &converted); - - /* The base conversion leaves the words on the heap, so copy them into the - * arena. */ - if (integer->length > 2) { - uint32_t *heap_words = integer->words.values; - integer->words.values = (uint32_t *) pm_arena_memdup(arena, heap_words, sizeof(uint32_t) * integer->length, PRISM_ALIGNOF(uint32_t)); - xfree(heap_words); - } + assert(bignum.length > 2 && bignum.length <= capacity); + pm_arena_shrink(arena, bignum.words, sizeof(uint32_t) * capacity, sizeof(uint32_t) * bignum.length); + *integer = (pm_integer_t) { .length = bignum.length, .words = { .values = bignum.words } }; } /** @@ -481,29 +143,29 @@ pm_integer_parse_decimal(pm_arena_t *arena, pm_integer_t *integer, size_t digits */ void pm_integer_parse(pm_arena_t *arena, pm_integer_t *integer, pm_integer_base_t base, const uint8_t *start, const uint8_t *end) { - // Ignore unary +. Unary - is parsed differently and will not end up here. - // Instead, it will modify the parsed integer later. + /* Ignore unary +. Unary - is parsed differently and will not end up here. + * Instead, it will modify the parsed integer later. */ if (*start == '+') start++; - // Determine the multiplier from the base, and skip past any prefixes. + /* Determine the multiplier from the base, and skip past any prefixes. */ uint32_t multiplier = 10; switch (base) { case PM_INTEGER_BASE_DEFAULT: break; case PM_INTEGER_BASE_BINARY: - start += 2; // 0b + start += 2; /* 0b */ multiplier = 2; break; case PM_INTEGER_BASE_OCTAL: - start++; // 0 - if (*start == '_' || *start == 'o' || *start == 'O') start++; // o + start++; /* 0 */ + if (*start == '_' || *start == 'o' || *start == 'O') start++; /* o */ multiplier = 8; break; case PM_INTEGER_BASE_DECIMAL: - if (*start == '0' && (end - start) > 1) start += 2; // 0d + if (*start == '0' && (end - start) > 1) start += 2; /* 0d */ break; case PM_INTEGER_BASE_HEXADECIMAL: - start += 2; // 0x + start += 2; /* 0x */ multiplier = 16; break; } @@ -553,6 +215,17 @@ pm_integer_parse(pm_arena_t *arena, pm_integer_t *integer, pm_integer_base_t bas *integer = pm_integer_uint64(value); } +/** + * Return a view of the magnitude of an integer as a bignum. The view's words + * point into the integer, so it must only be read. + */ +static pm_bignum_t +pm_integer_bignum(const pm_integer_t *integer) { + uint32_t length; + const uint32_t *words = pm_integer_words(integer, &length); + return (pm_bignum_t) { .length = length, .words = (uint32_t *) words }; +} + /** * Compare two integers. This function returns -1 if the left integer is less * than the right integer, 0 if they are equal, and 1 if the left integer is @@ -563,44 +236,30 @@ pm_integer_compare(const pm_integer_t *left, const pm_integer_t *right) { if (left->negative != right->negative) return left->negative ? -1 : 1; int negative = left->negative ? -1 : 1; - if (left->length < right->length) return -1 * negative; - if (left->length > right->length) return 1 * negative; - - uint32_t length; - const uint32_t *left_words = pm_integer_words(left, &length); - const uint32_t *right_words = pm_integer_words(right, &length); - - for (uint32_t index = 0; index < length; index++) { - uint32_t value_index = length - index - 1; - uint32_t left_value = left_words[value_index]; - uint32_t right_value = right_words[value_index]; - - if (left_value < right_value) return -1 * negative; - if (left_value > right_value) return 1 * negative; - } - - return 0; + pm_bignum_t left_bignum = pm_integer_bignum(left); + pm_bignum_t right_bignum = pm_integer_bignum(right); + return pm_bignum_compare(&left_bignum, &right_bignum) * negative; } /** * Reduce a ratio of integers to its simplest form. */ void pm_integers_reduce(pm_integer_t *numerator, pm_integer_t *denominator) { - // If either the numerator or denominator do not fit into a 32-bit integer, - // then this function is a no-op. In the future, we may consider reducing - // even the larger numbers, but for now we're going to keep it simple. + /* If either the numerator or denominator do not fit into a 32-bit integer, + * then this function is a no-op. In the future, we may consider reducing + * even the larger numbers, but for now we're going to keep it simple. */ if ( - // If the numerator doesn't fit into a 32-bit integer, return early. + /* If the numerator doesn't fit into a 32-bit integer, return early. */ numerator->length > 1 || - // If the denominator doesn't fit into a 32-bit integer, return early. + /* If the denominator doesn't fit into a 32-bit integer, return early. */ denominator->length > 1 || - // If the numerator is 0, then return early. + /* If the numerator is 0, then return early. */ numerator->length == 0 || - // If the denominator is 1, then return early. + /* If the denominator is 1, then return early. */ denominator->words.value[0] == 1 ) return; - // Find the greatest common divisor of the numerator and denominator. + /* Find the greatest common divisor of the numerator and denominator. */ uint32_t divisor = numerator->words.value[0]; uint32_t remainder = denominator->words.value[0]; @@ -610,7 +269,7 @@ void pm_integers_reduce(pm_integer_t *numerator, pm_integer_t *denominator) { divisor = temporary; } - // Divide the numerator and denominator by the greatest common divisor. + /* Divide the numerator and denominator by the greatest common divisor. */ numerator->words.value[0] /= divisor; denominator->words.value[0] /= divisor; } @@ -632,30 +291,38 @@ pm_integer_string(pm_buffer_t *buffer, const pm_integer_t *integer) { return; } - // Otherwise, first we'll convert the base from 1<<32 to 10**9. - pm_bignum_t converted = { 0 }; - pm_bignum_convert_base(&converted, &((pm_bignum_t) { integer->length, integer->words.values }), (uint64_t) 1 << 32, 1000000000); + /* Dividing by 10**9 yields the next nine digits from the remainder, least + * significant first, so the digits are written from the end of the buffer + * toward the front. The division is destructive, so it works on a copy. */ + const size_t words_size = sizeof(uint32_t) * integer->length; - // Allocate a buffer that we'll copy the decimal digits into. - const size_t digits_length = (size_t) converted.length * 9; - char *digits = xcalloc(digits_length, sizeof(char)); - if (digits == NULL) PRISM_OOM(); + /* A number of n words has at most n * 32 * log10(2) + 1 decimal digits, + * which is at most n * 10 for the three or more words that reach here. */ + const size_t capacity = (size_t) integer->length * 10; - // Pack bigdecimal to digits. - for (size_t value_index = 0; value_index < converted.length; value_index++) { - uint32_t value = converted.words[value_index]; + /* The copy of the words and the digits share one allocation, with the + * words first so that they are aligned. */ + void *allocation = xmalloc(words_size + capacity); + if (allocation == NULL) PRISM_OOM(); - for (size_t digit_index = 0; digit_index < 9; digit_index++) { - digits[digits_length - 9 * value_index - digit_index - 1] = (char) ('0' + value % 10); - value /= 10; + pm_bignum_t bignum = { .length = integer->length, .words = (uint32_t *) allocation }; + memcpy(bignum.words, integer->words.values, words_size); + + char *digits = (char *) allocation + words_size; + size_t offset = capacity; + + while (bignum.length > 0) { + uint32_t remainder = pm_bignum_div(&bignum, 1000000000); + + /* Every chunk but the most significant is exactly nine digits, padded + * with zeros. The most significant chunk is nonzero and stops at its + * leading digit. */ + for (size_t index = 0; index < 9 && (bignum.length > 0 || remainder > 0); index++) { + digits[--offset] = (char) ('0' + remainder % 10); + remainder /= 10; } } - size_t start_offset = 0; - while (start_offset < digits_length - 1 && digits[start_offset] == '0') start_offset++; - - // Finally, append the string to the buffer and free the digits. - pm_buffer_append_string(buffer, digits + start_offset, digits_length - start_offset); - xfree_sized(digits, sizeof(char) * digits_length); - pm_bignum_free(&converted); + pm_buffer_append_string(buffer, digits + offset, capacity - offset); + xfree_sized(allocation, words_size + capacity); } From cb6453247625db91ea2b53091c879dcc22b3b5b9 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 28 Oct 2026 12:24:29 -0400 Subject: [PATCH 04/14] Parse rationals and properly reduce --- include/prism/internal/bignum.h | 85 ++++++++++++ include/prism/internal/integer.h | 13 +- src/integer.c | 217 +++++++++++++++++++++---------- src/prism.c | 33 +---- 4 files changed, 235 insertions(+), 113 deletions(-) diff --git a/include/prism/internal/bignum.h b/include/prism/internal/bignum.h index e8a0a5321a..4700eca80a 100644 --- a/include/prism/internal/bignum.h +++ b/include/prism/internal/bignum.h @@ -7,8 +7,13 @@ #include "prism/compiler/inline.h" +#include "prism/internal/bit.h" + #include +#include +#include #include +#include /* * An arbitrary-precision unsigned integer. @@ -63,6 +68,86 @@ pm_bignum_div(pm_bignum_t *bignum, uint32_t divisor) { return (uint32_t) remainder; } +/* + * Divide the number by divisor if it divides evenly, and return whether it did. + * Otherwise the number is left unchanged: multiplying the quotient back and + * adding the remainder restores it exactly. + */ +static PRISM_INLINE bool +pm_bignum_div_exact(pm_bignum_t *bignum, uint32_t divisor) { + uint32_t remainder = pm_bignum_div(bignum, divisor); + if (remainder == 0) return true; + + pm_bignum_mul_add(bignum, divisor, remainder); + return false; +} + +/* + * Shift the number left by count bits, in place. The words grow by count / 32 + * plus possibly one more, so the storage must have room for them. + */ +static PRISM_INLINE void +pm_bignum_shift_left(pm_bignum_t *bignum, size_t count) { + if (bignum->length == 0) return; + + uint32_t *words = bignum->words; + uint32_t length = bignum->length; + uint32_t word_shift = (uint32_t) (count / 32); + uint32_t bit_shift = (uint32_t) (count % 32); + + /* Work down from the top word, so each word is read before the word that + * replaces it is written. */ + uint32_t carry = bit_shift == 0 ? 0 : words[length - 1] >> (32 - bit_shift); + for (uint32_t index = length; index > 0; index--) { + uint32_t below = (bit_shift == 0 || index == 1) ? 0 : words[index - 2] >> (32 - bit_shift); + words[index - 1 + word_shift] = (words[index - 1] << bit_shift) | below; + } + + memset(words, 0, sizeof(uint32_t) * word_shift); + bignum->length = length + word_shift; + if (carry != 0) words[bignum->length++] = carry; +} + +/* + * Shift the number right by count bits, in place, dropping the bits that are + * shifted out. + */ +static PRISM_INLINE void +pm_bignum_shift_right(pm_bignum_t *bignum, size_t count) { + uint32_t *words = bignum->words; + uint32_t word_shift = (uint32_t) (count / 32); + uint32_t bit_shift = (uint32_t) (count % 32); + + if (word_shift >= bignum->length) { + bignum->length = 0; + return; + } + + /* Work up from the bottom word, so each word is read before the word that + * replaces it is written. */ + uint32_t length = bignum->length - word_shift; + for (uint32_t index = 0; index < length; index++) { + uint32_t above = (bit_shift == 0 || index + 1 == length) ? 0 : words[index + word_shift + 1] << (32 - bit_shift); + words[index] = (words[index + word_shift] >> bit_shift) | above; + } + + bignum->length = length; + if (words[length - 1] == 0) bignum->length--; +} + +/* + * Return the number of trailing zero bits in a nonzero number. Its top word is + * nonzero, so the scan for the first nonzero word stops within its length. + */ +static PRISM_INLINE size_t +pm_bignum_trailing_zeros(const pm_bignum_t *bignum) { + assert(bignum->length > 0 && bignum->words[bignum->length - 1] != 0); + + size_t index = 0; + while (bignum->words[index] == 0) index++; + return index * 32 + pm_ctzll(bignum->words[index]); +} + /* * Compare two numbers. Returns -1 if left is less than right, 0 if they are * equal, and 1 if left is greater than right. diff --git a/include/prism/internal/integer.h b/include/prism/internal/integer.h index 148a0a00e5..9acc940c08 100644 --- a/include/prism/internal/integer.h +++ b/include/prism/internal/integer.h @@ -25,9 +25,6 @@ pm_integer_uint64(uint64_t value) { * from the string itself. */ typedef enum { - /* The default decimal base, with no prefix. Leading 0s will be ignored. */ - PM_INTEGER_BASE_DEFAULT, - /* The binary base, indicated by a 0b or 0B prefix. */ PM_INTEGER_BASE_BINARY, @@ -57,13 +54,11 @@ void pm_integer_parse(pm_arena_t *arena, pm_integer_t *integer, pm_integer_base_ int pm_integer_compare(const pm_integer_t *left, const pm_integer_t *right); /* - * Reduce a ratio of integers to its simplest form. - * - * If either the numerator or denominator do not fit into a 32-bit integer, then - * this function is a no-op. In the future, we may consider reducing even the - * larger numbers, but for now we're going to keep it simple. + * Parse a float rational literal like 1.5r, given its digits and decimal point + * without the r suffix, into a numerator and denominator in lowest terms. Words + * of either that do not fit in two words are allocated from the given arena. */ -void pm_integers_reduce(pm_integer_t *numerator, pm_integer_t *denominator); +void pm_integer_parse_rational(pm_arena_t *arena, pm_integer_t *numerator, pm_integer_t *denominator, const uint8_t *start, const uint8_t *end); /* Convert an integer to a decimal string. */ void pm_integer_string(pm_buffer_t *buffer, const pm_integer_t *integer); diff --git a/src/integer.c b/src/integer.c index a6d9f31389..2fa9898572 100644 --- a/src/integer.c +++ b/src/integer.c @@ -59,6 +59,34 @@ pm_integer_parse_digit(const uint8_t character) { return (uint8_t) value; } +/** + * Whether a character between the digits of a number is skipped. Underscores + * separate digits, and the decimal point of a float rational like 1.5r does not + * change its numerator, which is all of its digits. + */ +static PRISM_INLINE bool +pm_integer_separator(uint8_t character) { + return character == '_' || character == '.'; +} + +/** + * Store a bignum whose words are the most recent allocation in the arena, with + * room for capacity words, into an integer. An integer with at most two words + * stores them inline, so all of the arena words are given back; otherwise only + * the unused ones are. + */ +static void +pm_integer_from_bignum(pm_arena_t *arena, pm_integer_t *integer, const pm_bignum_t *bignum, size_t capacity) { + if (bignum->length <= 2) { + uint64_t value = (bignum->length > 0 ? (uint64_t) bignum->words[0] : 0) | (bignum->length > 1 ? (uint64_t) bignum->words[1] << 32 : 0); + pm_arena_shrink(arena, bignum->words, sizeof(uint32_t) * capacity, 0); + *integer = pm_integer_uint64(value); + } else { + pm_arena_shrink(arena, bignum->words, sizeof(uint32_t) * capacity, sizeof(uint32_t) * bignum->length); + *integer = (pm_integer_t) { .length = bignum->length, .words = { .values = bignum->words } }; + } +} + /** * Parse the digits of an integer in a power-of-two base that does not fit in * two words. Every digit but the leading one contributes exactly the same number @@ -78,7 +106,7 @@ pm_integer_parse_powof2(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, size_t bit_position = 0; for (const uint8_t *cursor = end; cursor > start;) { - if (*--cursor == '_') continue; + if (pm_integer_separator(*--cursor)) continue; uint32_t value = pm_integer_parse_digit(*cursor); size_t index = bit_position / 32; @@ -119,7 +147,7 @@ pm_integer_parse_decimal(pm_arena_t *arena, pm_integer_t *integer, size_t digits uint32_t chunk = 0; for (const uint8_t *cursor = start; cursor < end; cursor++) { - if (*cursor == '_') continue; + if (pm_integer_separator(*cursor)) continue; chunk = chunk * 10 + pm_integer_parse_digit(*cursor); if (++chunk_digits == chunk_size) { @@ -132,47 +160,17 @@ pm_integer_parse_decimal(pm_arena_t *arena, pm_integer_t *integer, size_t digits } assert(bignum.length > 2 && bignum.length <= capacity); - pm_arena_shrink(arena, bignum.words, sizeof(uint32_t) * capacity, sizeof(uint32_t) * bignum.length); - *integer = (pm_integer_t) { .length = bignum.length, .words = { .values = bignum.words } }; + pm_integer_from_bignum(arena, integer, &bignum, capacity); } /** - * Parse an integer from a string. This assumes that the format of the integer - * has already been validated, as internal validation checks are not performed - * here. + * Parse the digits of an integer in the given base, after any prefix. */ -void -pm_integer_parse(pm_arena_t *arena, pm_integer_t *integer, pm_integer_base_t base, const uint8_t *start, const uint8_t *end) { - /* Ignore unary +. Unary - is parsed differently and will not end up here. - * Instead, it will modify the parsed integer later. */ - if (*start == '+') start++; - - /* Determine the multiplier from the base, and skip past any prefixes. */ - uint32_t multiplier = 10; - switch (base) { - case PM_INTEGER_BASE_DEFAULT: - break; - case PM_INTEGER_BASE_BINARY: - start += 2; /* 0b */ - multiplier = 2; - break; - case PM_INTEGER_BASE_OCTAL: - start++; /* 0 */ - if (*start == '_' || *start == 'o' || *start == 'O') start++; /* o */ - multiplier = 8; - break; - case PM_INTEGER_BASE_DECIMAL: - if (*start == '0' && (end - start) > 1) start += 2; /* 0d */ - break; - case PM_INTEGER_BASE_HEXADECIMAL: - start += 2; /* 0x */ - multiplier = 16; - break; - } - +static void +pm_integer_parse_digits(pm_arena_t *arena, pm_integer_t *integer, uint32_t multiplier, const uint8_t *start, const uint8_t *end) { /* Leading zeros do not change the value, so skip them here and the paths * below always start at a nonzero digit. */ - while (start < end && (*start == '0' || *start == '_')) start++; + while (start < end && (*start == '0' || pm_integer_separator(*start))) start++; /* Everything has been consumed if the integer is zero, or if it is invalid * and has no digits. Either way the result is zero. */ @@ -191,13 +189,13 @@ pm_integer_parse(pm_arena_t *arena, pm_integer_t *integer, pm_integer_base_t bas uint64_t value = (uint64_t) pm_integer_parse_digit(*cursor++); for (; cursor < end; cursor++) { - if (*cursor == '_') continue; + if (pm_integer_separator(*cursor)) continue; uint64_t digit = (uint64_t) pm_integer_parse_digit(*cursor); if (value > limit || (value == limit && digit > limit_digit)) { size_t digits = 0; for (const uint8_t *scan = start; scan < end; scan++) { - if (*scan != '_') digits++; + if (!pm_integer_separator(*scan)) digits++; } if (multiplier == 10) { @@ -215,6 +213,114 @@ pm_integer_parse(pm_arena_t *arena, pm_integer_t *integer, pm_integer_base_t bas *integer = pm_integer_uint64(value); } +/** + * Parse an integer from a string. This assumes that the format of the integer + * has already been validated, as internal validation checks are not performed + * here. + */ +void +pm_integer_parse(pm_arena_t *arena, pm_integer_t *integer, pm_integer_base_t base, const uint8_t *start, const uint8_t *end) { + /* Ignore unary +. Unary - is parsed differently and will not end up here. + * Instead, it will modify the parsed integer later. */ + if (*start == '+') start++; + + /* Determine the multiplier from the base, and skip past any prefixes. */ + uint32_t multiplier = 10; + switch (base) { + case PM_INTEGER_BASE_BINARY: + start += 2; /* 0b */ + multiplier = 2; + break; + case PM_INTEGER_BASE_OCTAL: + start++; /* 0 */ + if (*start == '_' || *start == 'o' || *start == 'O') start++; /* o */ + multiplier = 8; + break; + case PM_INTEGER_BASE_DECIMAL: + if (*start == '0' && (end - start) > 1) start += 2; /* 0d */ + break; + case PM_INTEGER_BASE_HEXADECIMAL: + start += 2; /* 0x */ + multiplier = 16; + break; + } + + pm_integer_parse_digits(arena, integer, multiplier, start, end); +} + +/** + * Parse a float rational literal like 1.5r, given its digits and decimal point + * without the r suffix, into a numerator and denominator in lowest terms. Its + * value is its digits read as an integer over 10**k, where k is the number of + * digits after the decimal point. Since 10**k is 2**k * 5**k, the greatest + * common divisor of the two is a power of two times a power of five, so the + * fraction is reduced by dividing those out of the numerator and leaving them + * out of the denominator. + */ +void +pm_integer_parse_rational(pm_arena_t *arena, pm_integer_t *numerator, pm_integer_t *denominator, const uint8_t *start, const uint8_t *end) { + static const uint32_t powof5[14] = { 1, 5, 25, 125, 625, 3125, 15625, 78125, 390625, 1953125, 9765625, 48828125, 244140625, 1220703125 }; + + const uint8_t *point = memchr(start, '.', (size_t) (end - start)); + assert(point != NULL); + + /* Each trailing zero after the decimal point divides both sides by 10, so + * drop them before parsing. */ + while (end > point + 1 && (end[-1] == '0' || end[-1] == '_')) end--; + + size_t exponent = 0; + for (const uint8_t *cursor = point + 1; cursor < end; cursor++) { + if (*cursor != '_') exponent++; + } + + pm_integer_parse_digits(arena, numerator, 10, start, end); + size_t twos = 0; + size_t fives = 0; + + if (numerator->length > 0 && exponent > 0) { + uint32_t length = numerator->length; + pm_bignum_t bignum = { + .length = length, + .words = length <= 2 ? numerator->words.value : numerator->words.values + }; + + twos = pm_bignum_trailing_zeros(&bignum); + if (twos > exponent) twos = exponent; + pm_bignum_shift_right(&bignum, twos); + + /* 5**13 is the largest power of five that fits in a word, so divide by + * it while it divides evenly before dividing by five alone. */ + while (exponent - fives >= 13 && pm_bignum_div_exact(&bignum, powof5[13])) fives += 13; + while (fives < exponent && pm_bignum_div_exact(&bignum, 5)) fives++; + + if (length <= 2) { + uint64_t value = (bignum.length > 0 ? (uint64_t) bignum.words[0] : 0) | (bignum.length > 1 ? (uint64_t) bignum.words[1] << 32 : 0); + *numerator = pm_integer_uint64(value); + } else { + pm_integer_from_bignum(arena, numerator, &bignum, length); + } + } + + /* The denominator is what is left of 10**exponent. 2378/1024 is just above + * log2(5), so this capacity never undercounts. */ + size_t remaining_fives = exponent - fives; + size_t remaining_twos = exponent - twos; + size_t capacity = (remaining_fives * 2378 / 1024 + remaining_twos + 32) / 32; + + pm_bignum_t bignum = { + .length = 1, + .words = (uint32_t *) pm_arena_alloc(arena, sizeof(uint32_t) * capacity, PRISM_ALIGNOF(uint32_t)) + }; + bignum.words[0] = 1; + + for (; remaining_fives >= 13; remaining_fives -= 13) pm_bignum_mul_add(&bignum, powof5[13], 0); + if (remaining_fives > 0) pm_bignum_mul_add(&bignum, powof5[remaining_fives], 0); + pm_bignum_shift_left(&bignum, remaining_twos); + + assert(bignum.length <= capacity); + pm_integer_from_bignum(arena, denominator, &bignum, capacity); +} + /** * Return a view of the magnitude of an integer as a bignum. The view's words * point into the integer, so it must only be read. @@ -241,39 +347,6 @@ pm_integer_compare(const pm_integer_t *left, const pm_integer_t *right) { return pm_bignum_compare(&left_bignum, &right_bignum) * negative; } -/** - * Reduce a ratio of integers to its simplest form. - */ -void pm_integers_reduce(pm_integer_t *numerator, pm_integer_t *denominator) { - /* If either the numerator or denominator do not fit into a 32-bit integer, - * then this function is a no-op. In the future, we may consider reducing - * even the larger numbers, but for now we're going to keep it simple. */ - if ( - /* If the numerator doesn't fit into a 32-bit integer, return early. */ - numerator->length > 1 || - /* If the denominator doesn't fit into a 32-bit integer, return early. */ - denominator->length > 1 || - /* If the numerator is 0, then return early. */ - numerator->length == 0 || - /* If the denominator is 1, then return early. */ - denominator->words.value[0] == 1 - ) return; - - /* Find the greatest common divisor of the numerator and denominator. */ - uint32_t divisor = numerator->words.value[0]; - uint32_t remainder = denominator->words.value[0]; - - while (remainder != 0) { - uint32_t temporary = remainder; - remainder = divisor % remainder; - divisor = temporary; - } - - /* Divide the numerator and denominator by the greatest common divisor. */ - numerator->words.value[0] /= divisor; - denominator->words.value[0] /= divisor; -} - /** * Convert an integer to a decimal string. */ diff --git a/src/prism.c b/src/prism.c index 3d7bebd50b..1188c8bc34 100644 --- a/src/prism.c +++ b/src/prism.c @@ -4228,38 +4228,7 @@ pm_float_node_rational_create(pm_parser_t *parser, const pm_token_t *token) { ((pm_integer_t) { 0 }) ); - const uint8_t *start = token->start; - const uint8_t *end = token->end - 1; // r - - while (start < end && *start == '0') start++; // 0.1 -> .1 - while (end > start && end[-1] == '0') end--; // 1.0 -> 1. - - size_t length = (size_t) (end - start); - if (length == 1) { - node->denominator = pm_integer_uint64(1); - return node; - } - - const uint8_t *point = memchr(start, '.', length); - assert(point && "should have a decimal point"); - - uint8_t *digits = xmalloc(length); - if (digits == NULL) PRISM_OOM(); - - memcpy(digits, start, (unsigned long) (point - start)); - memcpy(digits + (point - start), point + 1, (unsigned long) (end - point - 1)); - pm_integer_parse(parser->arena, &node->numerator, PM_INTEGER_BASE_DEFAULT, digits, digits + length - 1); - - size_t fract_length = 0; - for (const uint8_t *fract = point; fract < end; ++fract) { - if (*fract != '_') ++fract_length; - } - digits[0] = '1'; - if (fract_length > 1) memset(digits + 1, '0', fract_length - 1); - pm_integer_parse(parser->arena, &node->denominator, PM_INTEGER_BASE_DEFAULT, digits, digits + fract_length); - xfree_sized(digits, length); - - pm_integers_reduce(&node->numerator, &node->denominator); + pm_integer_parse_rational(parser->arena, &node->numerator, &node->denominator, token->start, token->end - 1); return node; } From d22a7f3181ed715375f65ddc52220eae14483458 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 29 Oct 2026 01:42:51 -0400 Subject: [PATCH 05/14] Use rb_integer_unpack for large numbers when reifying --- templates/ext/prism/api_node.c.erb | 34 ++++++++++-------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/templates/ext/prism/api_node.c.erb b/templates/ext/prism/api_node.c.erb index 77e43179df..84e16c7e1b 100644 --- a/templates/ext/prism/api_node.c.erb +++ b/templates/ext/prism/api_node.c.erb @@ -48,31 +48,19 @@ pm_string_new(const pm_string_t *string, rb_encoding *encoding) { VALUE pm_integer_new(const pm_integer_t *integer) { - VALUE result; - if (integer->length <= 2) { - result = ULL2NUM(((unsigned long long) integer->words.value[1] << 32) | integer->words.value[0]); - } else { - VALUE string = rb_str_new(NULL, integer->length * 8); - unsigned char *bytes = (unsigned char *) RSTRING_PTR(string); - - size_t offset = integer->length * 8; - for (size_t value_index = 0; value_index < integer->length; value_index++) { - uint32_t value = integer->words.values[value_index]; - - for (int index = 0; index < 8; index++) { - int byte = (value >> (4 * index)) & 0xf; - bytes[--offset] = byte < 10 ? byte + '0' : byte - 10 + 'a'; - } - } - - result = rb_funcall(string, rb_intern("to_i"), 1, UINT2NUM(16)); - } - - if (integer->negative) { - result = rb_funcall(result, rb_intern("-@"), 0); + uint32_t length; + const uint32_t *words = pm_integer_words(integer, &length); + + /* An integer whose words are inline fits in an unsigned long long, so it + * converts directly unless it is negative and too large to negate. */ + if (length <= 2) { + unsigned long long value = ((unsigned long long) integer->words.value[1] << 32) | integer->words.value[0]; + if (!integer->negative) return ULL2NUM(value); + if (value <= LLONG_MAX) return LL2NUM(-(long long) value); } - return result; + int flags = INTEGER_PACK_LSWORD_FIRST | INTEGER_PACK_NATIVE_BYTE_ORDER | (integer->negative ? INTEGER_PACK_NEGATIVE : 0); + return rb_integer_unpack(words, length, sizeof(uint32_t), 0, flags); } // Create a Prism::Source object from the given parser, after pm_parse() was called. From 534e615dc88ce1898a889eba8f1efaf82b3e1e0f Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 29 Oct 2026 10:41:05 -0400 Subject: [PATCH 06/14] Use enum value properly for base --- include/prism/internal/integer.h | 8 ++++---- src/integer.c | 22 +++++++++------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/include/prism/internal/integer.h b/include/prism/internal/integer.h index 9acc940c08..3d4a4a7e3c 100644 --- a/include/prism/internal/integer.h +++ b/include/prism/internal/integer.h @@ -26,16 +26,16 @@ pm_integer_uint64(uint64_t value) { */ typedef enum { /* The binary base, indicated by a 0b or 0B prefix. */ - PM_INTEGER_BASE_BINARY, + PM_INTEGER_BASE_BINARY = 2, /* The octal base, indicated by a 0, 0o, or 0O prefix. */ - PM_INTEGER_BASE_OCTAL, + PM_INTEGER_BASE_OCTAL = 8, /* The decimal base, indicated by a 0d, 0D, or empty prefix. */ - PM_INTEGER_BASE_DECIMAL, + PM_INTEGER_BASE_DECIMAL = 10, /* The hexadecimal base, indicated by a 0x or 0X prefix. */ - PM_INTEGER_BASE_HEXADECIMAL + PM_INTEGER_BASE_HEXADECIMAL = 16 } pm_integer_base_t; /* diff --git a/src/integer.c b/src/integer.c index 2fa9898572..05e98c9171 100644 --- a/src/integer.c +++ b/src/integer.c @@ -167,7 +167,7 @@ pm_integer_parse_decimal(pm_arena_t *arena, pm_integer_t *integer, size_t digits * Parse the digits of an integer in the given base, after any prefix. */ static void -pm_integer_parse_digits(pm_arena_t *arena, pm_integer_t *integer, uint32_t multiplier, const uint8_t *start, const uint8_t *end) { +pm_integer_parse_digits(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, const uint8_t *start, const uint8_t *end) { /* Leading zeros do not change the value, so skip them here and the paths * below always start at a nonzero digit. */ while (start < end && (*start == '0' || pm_integer_separator(*start))) start++; @@ -180,10 +180,10 @@ pm_integer_parse_digits(pm_arena_t *arena, pm_integer_t *integer, uint32_t multi } /* Accumulate into a uint64_t, which covers every integer whose words are - * stored inline. value * multiplier + digit overflows exactly when value + * stored inline. value * base + digit overflows exactly when value * is past limit, or equal to it with digit past limit_digit. */ - const uint64_t limit = UINT64_MAX / multiplier; - const uint64_t limit_digit = UINT64_MAX % multiplier; + const uint64_t limit = UINT64_MAX / base; + const uint64_t limit_digit = UINT64_MAX % base; const uint8_t *cursor = start; uint64_t value = (uint64_t) pm_integer_parse_digit(*cursor++); @@ -198,16 +198,16 @@ pm_integer_parse_digits(pm_arena_t *arena, pm_integer_t *integer, uint32_t multi if (!pm_integer_separator(*scan)) digits++; } - if (multiplier == 10) { + if (base == 10) { pm_integer_parse_decimal(arena, integer, digits, start, end); } else { - pm_integer_parse_powof2(arena, integer, multiplier, digits, start, end); + pm_integer_parse_powof2(arena, integer, base, digits, start, end); } return; } - value = value * multiplier + digit; + value = value * base + digit; } *integer = pm_integer_uint64(value); @@ -224,28 +224,24 @@ pm_integer_parse(pm_arena_t *arena, pm_integer_t *integer, pm_integer_base_t bas * Instead, it will modify the parsed integer later. */ if (*start == '+') start++; - /* Determine the multiplier from the base, and skip past any prefixes. */ - uint32_t multiplier = 10; + /* Skip past any prefixes. */ switch (base) { case PM_INTEGER_BASE_BINARY: start += 2; /* 0b */ - multiplier = 2; break; case PM_INTEGER_BASE_OCTAL: start++; /* 0 */ if (*start == '_' || *start == 'o' || *start == 'O') start++; /* o */ - multiplier = 8; break; case PM_INTEGER_BASE_DECIMAL: if (*start == '0' && (end - start) > 1) start += 2; /* 0d */ break; case PM_INTEGER_BASE_HEXADECIMAL: start += 2; /* 0x */ - multiplier = 16; break; } - pm_integer_parse_digits(arena, integer, multiplier, start, end); + pm_integer_parse_digits(arena, integer, (uint32_t) base, start, end); } /** From 315f5a14965f7855056e998723ed444e784a8d9d Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 29 Oct 2026 11:24:26 -0400 Subject: [PATCH 07/14] Key lexer integer cache on position --- include/prism/internal/parser.h | 17 +++++++++++------ src/prism.c | 18 ++++++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/include/prism/internal/parser.h b/include/prism/internal/parser.h index 28d895282e..c3016d7c46 100644 --- a/include/prism/internal/parser.h +++ b/include/prism/internal/parser.h @@ -812,15 +812,20 @@ struct pm_parser_t { pm_node_flags_t base; /* - * When lexing a decimal integer that fits in a uint64_t, we compute - * the value during lexing to avoid re-scanning the digits during - * parsing. If lexed is true, this holds the result and - * pm_integer_parse can be skipped. + * When lexing a decimal integer that fits in a uint64_t, or a plain 0, + * we compute the value during lexing to avoid re-scanning the digits + * during parsing. If start is set, this holds the result for the + * token that begins there and pm_integer_parse can be skipped. */ uint64_t value; - /* Whether value holds a valid pre-computed integer. */ - bool lexed; + /* + * The start of the token whose value is held in value, or NULL before + * any has been. The parser creates the node for a token only after + * lexing the next one, which may be another integer that replaces the + * value, so node creation checks that it belongs to the token at hand. + */ + const uint8_t *start; } integer; /* diff --git a/src/prism.c b/src/prism.c index 1188c8bc34..ff228368ed 100644 --- a/src/prism.c +++ b/src/prism.c @@ -4724,10 +4724,9 @@ pm_integer_node_create(pm_parser_t *parser, pm_node_flags_t base, const pm_token ((pm_integer_t) { 0 }) ); - if (parser->integer.lexed) { + if (parser->integer.start == token->start) { // The value was already computed during lexing. node->value = pm_integer_uint64(parser->integer.value); - parser->integer.lexed = false; } else { pm_integer_base_t integer_base = PM_INTEGER_BASE_DECIMAL; switch (base) { @@ -8235,6 +8234,12 @@ lex_numeric_prefix(pm_parser_t *parser, bool* seen_e) { type = lex_optional_float_suffix(parser, seen_e); break; } + + /* Anything else ends the literal, which is just 0. */ + default: + parser->integer.value = 0; + parser->integer.start = parser->current.start; + break; } } else { // If it didn't start with a 0, then we'll lex as far as we can into a @@ -8275,7 +8280,7 @@ lex_numeric_prefix(pm_parser_t *parser, bool* seen_e) { size_t digits = (size_t) (cursor - parser->current.end) + 1 - underscores; if (digits <= 19) { parser->integer.value = value; - parser->integer.lexed = true; + parser->integer.start = parser->current.start; } parser->current.end = cursor; @@ -8289,12 +8294,6 @@ lex_numeric_prefix(pm_parser_t *parser, bool* seen_e) { uint8_t next = peek(parser); if (next == '.' || next == 'e' || next == 'E') { type = lex_optional_float_suffix(parser, seen_e); - - // If it turned out to be a float, the cached integer value is - // invalid. - if (type != PM_TOKEN_INTEGER) { - parser->integer.lexed = false; - } } } } @@ -8316,7 +8315,6 @@ static pm_token_type_t lex_numeric(pm_parser_t *parser) { pm_token_type_t type = PM_TOKEN_INTEGER; parser->integer.base = PM_INTEGER_BASE_FLAGS_DECIMAL; - parser->integer.lexed = false; if (parser->current.end < parser->end) { bool seen_e = false; From 0c3e66cfe9beb9f07b418461638b803a29364cad Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 1 Nov 2026 14:07:48 -0500 Subject: [PATCH 08/14] Bring back karatsuba parsing for large integers --- include/prism/internal/bignum.h | 43 ++++- include/prism/internal/bit.h | 44 ++++-- prism.gemspec | 1 + src/bignum.c | 200 ++++++++++++++++++++++++ src/integer.c | 193 +++++++++++++++++++---- test/prism/result/integer_parse_test.rb | 3 + 6 files changed, 443 insertions(+), 41 deletions(-) create mode 100644 src/bignum.c diff --git a/include/prism/internal/bignum.h b/include/prism/internal/bignum.h index 4700eca80a..fd2eaac4f3 100644 --- a/include/prism/internal/bignum.h +++ b/include/prism/internal/bignum.h @@ -139,13 +139,13 @@ pm_bignum_shift_right(pm_bignum_t *bignum, size_t count) { * Return the number of trailing zero bits in a nonzero number. Its top word is * nonzero, so the scan for the first nonzero word stops within its length. */ -static PRISM_INLINE size_t +static PRISM_INLINE uint64_t pm_bignum_trailing_zeros(const pm_bignum_t *bignum) { assert(bignum->length > 0 && bignum->words[bignum->length - 1] != 0); - size_t index = 0; + uint32_t index = 0; while (bignum->words[index] == 0) index++; - return index * 32 + pm_ctzll(bignum->words[index]); + return (uint64_t) index * 32 + pm_ctzll(bignum->words[index]); } /* @@ -165,4 +165,41 @@ pm_bignum_compare(const pm_bignum_t *left, const pm_bignum_t *right) { return 0; } +/* + * Temporary words for the operations that need them, taken and given back in + * stack order: an operation saves the cursor, takes what it needs, and + * restores the cursor before it returns. + */ +typedef struct { + /* The next free word. */ + uint32_t *cursor; + + /* One past the last word. */ + uint32_t *end; +} pm_bignum_scratch_t; + +/* + * Take count words from the scratch space. + */ +static PRISM_INLINE uint32_t * +pm_bignum_scratch_take(pm_bignum_scratch_t *scratch, uint32_t count) { + assert((size_t) (scratch->end - scratch->cursor) >= count); + uint32_t *words = scratch->cursor; + scratch->cursor += count; + return words; +} + +/* + * Add right into left, in place. The storage must have room for the sum, which + * is at most one word longer than the longer of the two. + */ +void pm_bignum_add(pm_bignum_t *left, const pm_bignum_t *right); + +/* + * Multiply left by right into result, whose storage has room for + * left->length + right->length words and overlaps neither. Long numbers use + * Karatsuba's method, which takes its temporaries from scratch. + */ +void pm_bignum_multiply(pm_bignum_t *result, const pm_bignum_t *left, const pm_bignum_t *right, pm_bignum_scratch_t *scratch); + #endif diff --git a/include/prism/internal/bit.h b/include/prism/internal/bit.h index 4f7b56090f..9f7d259de9 100644 --- a/include/prism/internal/bit.h +++ b/include/prism/internal/bit.h @@ -4,18 +4,30 @@ #include "prism/compiler/inline.h" /* - * Count trailing zero bits in a 64-bit value. Used by SWAR identifier scanning - * to find the first non-matching byte in a word. + * Count trailing zero bits in a 64-bit value, and find the index of the + * highest set bit in one (the result of the x86 bsr instruction). pm_ctzll is + * used by SWAR identifier scanning to find the first non-matching byte in a + * word, and pm_bsrll by integer parsing to find the largest power of two below + * a count. * - * Precondition: v must be nonzero. The result is undefined when v == 0 - * (matching the behavior of __builtin_ctzll and _BitScanForward64). + * Precondition: v must be nonzero. The results are undefined when v == 0 + * (matching the behavior of __builtin_ctzll, __builtin_clzll, + * _BitScanForward64, and _BitScanReverse64). */ #if defined(__GNUC__) || defined(__clang__) +#define pm_bsrll(v) (63 - (unsigned) __builtin_clzll(v)) #define pm_ctzll(v) ((unsigned) __builtin_ctzll(v)) #elif defined(_MSC_VER) && defined(_WIN64) #include #include +static PRISM_INLINE unsigned +pm_bsrll(uint64_t v) { + unsigned long index; + _BitScanReverse64(&index, v); + return (unsigned) index; +} + static PRISM_INLINE unsigned pm_ctzll(uint64_t v) { unsigned long index; @@ -25,16 +37,28 @@ pm_ctzll(uint64_t v) { #else #include +static PRISM_INLINE unsigned +pm_bsrll(uint64_t v) { + unsigned c = 0; + if (v & 0xFFFFFFFF00000000ULL) { c += 32; v >>= 32; } + if (v & 0x00000000FFFF0000ULL) { c += 16; v >>= 16; } + if (v & 0x000000000000FF00ULL) { c += 8; v >>= 8; } + if (v & 0x00000000000000F0ULL) { c += 4; v >>= 4; } + if (v & 0x000000000000000CULL) { c += 2; v >>= 2; } + if (v & 0x0000000000000002ULL) { c += 1; } + return c; +} + static PRISM_INLINE unsigned pm_ctzll(uint64_t v) { unsigned c = 0; v &= (uint64_t) (-(int64_t) v); - if (v & 0x00000000FFFFFFFFULL) c += 0; else c += 32; - if (v & 0x0000FFFF0000FFFFULL) c += 0; else c += 16; - if (v & 0x00FF00FF00FF00FFULL) c += 0; else c += 8; - if (v & 0x0F0F0F0F0F0F0F0FULL) c += 0; else c += 4; - if (v & 0x3333333333333333ULL) c += 0; else c += 2; - if (v & 0x5555555555555555ULL) c += 0; else c += 1; + if (!(v & 0x00000000FFFFFFFFULL)) c += 32; + if (!(v & 0x0000FFFF0000FFFFULL)) c += 16; + if (!(v & 0x00FF00FF00FF00FFULL)) c += 8; + if (!(v & 0x0F0F0F0F0F0F0F0FULL)) c += 4; + if (!(v & 0x3333333333333333ULL)) c += 2; + if (!(v & 0x5555555555555555ULL)) c += 1; return c; } #endif diff --git a/prism.gemspec b/prism.gemspec index 7af363b586..b1540b922e 100644 --- a/prism.gemspec +++ b/prism.gemspec @@ -217,6 +217,7 @@ Gem::Specification.new do |spec| "sig/generated/prism/parse_result/newlines.rbs", "sig/manifest.yaml", "src/arena.c", + "src/bignum.c", "src/buffer.c", "src/char.c", "src/comments.c", diff --git a/src/bignum.c b/src/bignum.c new file mode 100644 index 0000000000..9d9954dd8c --- /dev/null +++ b/src/bignum.c @@ -0,0 +1,200 @@ +#include "prism/internal/bignum.h" + +#include +#include +#include +#include + +/* + * Multiplications where the shorter number has fewer words than this use the + * schoolbook method, which is faster than Karatsuba's at small sizes. + */ +#define PM_BIGNUM_KARATSUBA_THRESHOLD 24 + +/* + * Add right into left, in place. The sum must fit in left's length words. + */ +static void +pm_bignum_words_add(uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + uint64_t carry = 0; + uint32_t index = 0; + + for (; index < right_length; index++) { + carry += (uint64_t) left[index] + right[index]; + left[index] = (uint32_t) carry; + carry >>= 32; + } + + for (; carry != 0; index++) { + assert(index < left_length); + carry += left[index]; + left[index] = (uint32_t) carry; + carry >>= 32; + } +} + +/* + * Subtract right from left, in place. Left must be at least right. A word minus + * a word minus a borrow wraps around to a value with its top bit set exactly + * when it is negative, so that bit is the next borrow. + */ +static void +pm_bignum_words_subtract(uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + uint64_t borrow = 0; + uint32_t index = 0; + + for (; index < right_length; index++) { + uint64_t difference = (uint64_t) left[index] - right[index] - borrow; + left[index] = (uint32_t) difference; + borrow = difference >> 63; + } + + for (; borrow != 0; index++) { + assert(index < left_length); + uint64_t difference = (uint64_t) left[index] - borrow; + left[index] = (uint32_t) difference; + borrow = difference >> 63; + } +} + +/* + * Multiply left by right into result, which has room for left_length + + * right_length words and overlaps neither, using the schoolbook method. A word + * times a word plus two words is at most 2**64 - 1, so each step fits in a + * uint64_t. + */ +static void +pm_bignum_words_multiply_schoolbook(uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + memset(result, 0, sizeof(uint32_t) * (left_length + right_length)); + + for (uint32_t right_index = 0; right_index < right_length; right_index++) { + uint64_t carry = 0; + + for (uint32_t left_index = 0; left_index < left_length; left_index++) { + carry += (uint64_t) left[left_index] * right[right_index] + result[left_index + right_index]; + result[left_index + right_index] = (uint32_t) carry; + carry >>= 32; + } + + result[right_index + left_length] = (uint32_t) carry; + } +} + +/* + * Multiply left by right into result, which has room for left_length + + * right_length words and overlaps neither. Above PM_BIGNUM_KARATSUBA_THRESHOLD + * this uses Karatsuba's method: splitting each number into low and high halves + * at the same word, low * low and high * high go straight into the bottom and + * top of the result, and the middle term is (low + high) * (low + high) minus + * those two, which takes three multiplications of half the size instead of + * four. + */ +static void +pm_bignum_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length, pm_bignum_scratch_t *scratch) { + if (left_length < right_length) { + const uint32_t *words = left; + left = right; + right = words; + + uint32_t length = left_length; + left_length = right_length; + right_length = length; + } + + if (right_length < PM_BIGNUM_KARATSUBA_THRESHOLD) { + pm_bignum_words_multiply_schoolbook(result, left, left_length, right, right_length); + return; + } + + uint32_t *saved = scratch->cursor; + + /* When left is at least twice as long, splitting both at the same word + * would leave the halves badly mismatched, so instead multiply right by + * each right-sized piece of left and add the products into the result. */ + if (left_length >= 2 * right_length) { + memset(result, 0, sizeof(uint32_t) * (left_length + right_length)); + uint32_t *product = pm_bignum_scratch_take(scratch, 2 * right_length); + + for (uint32_t offset = 0; offset < left_length; offset += right_length) { + uint32_t piece = left_length - offset < right_length ? left_length - offset : right_length; + pm_bignum_words_multiply(product, left + offset, piece, right, right_length, scratch); + pm_bignum_words_add(result + offset, left_length + right_length - offset, product, piece + right_length); + } + + scratch->cursor = saved; + return; + } + + uint32_t half = right_length / 2; + uint32_t left_high_length = left_length - half; + uint32_t right_high_length = right_length - half; + + pm_bignum_words_multiply(result, left, half, right, half, scratch); + pm_bignum_words_multiply(result + 2 * half, left + half, left_high_length, right + half, right_high_length, scratch); + + /* Each high half is at least as long as the low half, so adding the low + * half into a copy of the high half needs at most one more word. */ + uint32_t *left_sum = pm_bignum_scratch_take(scratch, left_high_length + 1); + memcpy(left_sum, left + half, sizeof(uint32_t) * left_high_length); + left_sum[left_high_length] = 0; + pm_bignum_words_add(left_sum, left_high_length + 1, left, half); + + uint32_t *right_sum = pm_bignum_scratch_take(scratch, right_high_length + 1); + memcpy(right_sum, right + half, sizeof(uint32_t) * right_high_length); + right_sum[right_high_length] = 0; + pm_bignum_words_add(right_sum, right_high_length + 1, right, half); + + uint32_t middle_length = left_high_length + right_high_length + 2; + uint32_t *middle = pm_bignum_scratch_take(scratch, middle_length); + pm_bignum_words_multiply(middle, left_sum, left_high_length + 1, right_sum, right_high_length + 1, scratch); + pm_bignum_words_subtract(middle, middle_length, result, 2 * half); + pm_bignum_words_subtract(middle, middle_length, result + 2 * half, left_high_length + right_high_length); + + while (middle_length > 0 && middle[middle_length - 1] == 0) middle_length--; + pm_bignum_words_add(result + half, left_length + right_length - half, middle, middle_length); + + scratch->cursor = saved; +} + +/* + * Add right into left, in place. The storage must have room for the sum, which + * is at most one word longer than the longer of the two. + */ +void +pm_bignum_add(pm_bignum_t *left, const pm_bignum_t *right) { + if (left->length < right->length) { + memset(left->words + left->length, 0, sizeof(uint32_t) * (right->length - left->length)); + left->length = right->length; + } + + uint64_t carry = 0; + uint32_t index = 0; + + for (; index < right->length; index++) { + carry += (uint64_t) left->words[index] + right->words[index]; + left->words[index] = (uint32_t) carry; + carry >>= 32; + } + + for (; carry != 0 && index < left->length; index++) { + carry += left->words[index]; + left->words[index] = (uint32_t) carry; + carry >>= 32; + } + + if (carry != 0) left->words[left->length++] = (uint32_t) carry; +} + +/* + * Multiply left by right into result, whose storage has room for + * left->length + right->length words and overlaps neither. Long numbers use + * Karatsuba's method, which takes its temporaries from scratch. + */ +void +pm_bignum_multiply(pm_bignum_t *result, const pm_bignum_t *left, const pm_bignum_t *right, pm_bignum_scratch_t *scratch) { + uint32_t length = left->length + right->length; + pm_bignum_words_multiply(result->words, left->words, left->length, right->words, right->length, scratch); + + while (length > 0 && result->words[length - 1] == 0) length--; + result->length = length; +} diff --git a/src/integer.c b/src/integer.c index 05e98c9171..2f964d3af6 100644 --- a/src/integer.c +++ b/src/integer.c @@ -1,6 +1,7 @@ #include "prism/internal/integer.h" #include "prism/compiler/align.h" +#include "prism/compiler/inline.h" #include "prism/internal/allocator.h" #include "prism/internal/arena.h" @@ -76,7 +77,7 @@ pm_integer_separator(uint8_t character) { * the unused ones are. */ static void -pm_integer_from_bignum(pm_arena_t *arena, pm_integer_t *integer, const pm_bignum_t *bignum, size_t capacity) { +pm_integer_from_bignum(pm_arena_t *arena, pm_integer_t *integer, const pm_bignum_t *bignum, uint32_t capacity) { if (bignum->length <= 2) { uint64_t value = (bignum->length > 0 ? (uint64_t) bignum->words[0] : 0) | (bignum->length > 1 ? (uint64_t) bignum->words[1] << 32 : 0); pm_arena_shrink(arena, bignum->words, sizeof(uint32_t) * capacity, 0); @@ -94,23 +95,23 @@ pm_integer_from_bignum(pm_arena_t *arena, pm_integer_t *integer, const pm_bignum * are packed directly into words allocated from the arena. */ static void -pm_integer_parse_powof2(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, size_t digits, const uint8_t *start, const uint8_t *end) { +pm_integer_parse_powof2(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, uint32_t digits, const uint8_t *start, const uint8_t *end) { static const uint8_t digit_bits[16] = { 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4 }; - size_t leading_bits = digit_bits[pm_integer_parse_digit(*start)]; + uint32_t leading_bits = digit_bits[pm_integer_parse_digit(*start)]; - size_t bit = pm_ctzll(base); - uint32_t length = (uint32_t) (((digits - 1) * bit + leading_bits + 31) / 32); + uint32_t bit = (uint32_t) pm_ctzll(base); + uint32_t length = (uint32_t) (((uint64_t) (digits - 1) * bit + leading_bits + 31) / 32); assert(length > 2); uint32_t *words = pm_arena_zalloc(arena, sizeof(uint32_t) * length, PRISM_ALIGNOF(uint32_t)); - size_t bit_position = 0; + uint64_t bit_position = 0; for (const uint8_t *cursor = end; cursor > start;) { if (pm_integer_separator(*--cursor)) continue; uint32_t value = pm_integer_parse_digit(*cursor); - size_t index = bit_position / 32; - size_t shift = bit_position % 32; + uint32_t index = (uint32_t) (bit_position / 32); + uint32_t shift = (uint32_t) (bit_position % 32); words[index] |= value << shift; if (32 - shift < bit && index + 1 < length) words[index + 1] |= value >> (32 - shift); @@ -121,6 +122,142 @@ pm_integer_parse_powof2(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, *integer = (pm_integer_t) { .length = length, .words = { .values = words } }; } +/* + * When parsing long decimal digits, we switch to a divide-and-conquer approach + * once it gets really long because we start to see the impact of an O(n^1.58) + * vs an O(n^2) algorithm. + */ +#define PM_INTEGER_DIVIDE_THRESHOLD 224 + +/* + * The number of words needed to hold a decimal integer with the given number + * of digits. A number with d digits is below 10**d, so it needs at most: + * + * d * log2(10) + 1 + * + * bits, which rounds up to at most one word more than the whole words below + * that count. To find log2(10), we approximate it as 3402/1024, which is + * slightly greater than the actual value so it does not undercount. + */ +static PRISM_INLINE uint32_t +pm_integer_decimal_words(uint64_t digits) { + return (uint32_t) ((digits * 3402 / 1024) / 32 + 1); +} + +/* + * The number of words of storage needed to convert nchunks chunks of nine + * decimal digits, which is one word more than the value needs. The conversion + * multiplies the high half by a power of ten into this storage, and + * pm_bignum_multiply writes the sum of its factors' lengths before it trims a + * zero top word. Each factor's length rounds its bits up to a whole word, so + * that sum can be one word more than the product needs. + */ +static PRISM_INLINE uint32_t +pm_integer_chunks_words(uint32_t nchunks) { + return pm_integer_decimal_words((uint64_t) nchunks * 9) + 1; +} + +/* + * Convert nchunks of nine decimal digits into a bignum. Do this by effectively + * converting the base of the number from 10**9 to 2**32. The result's storage + * must have room for pm_integer_chunks_words(nchunks) words. + */ +static void +pm_integer_parse_decimal_divide_chunks(pm_bignum_t *result, uint32_t nchunks, const uint32_t *chunks, const pm_bignum_t *powers, pm_bignum_scratch_t *scratch) { + if (nchunks <= PM_INTEGER_DIVIDE_THRESHOLD) { + result->length = 0; + for (uint32_t index = nchunks; index > 0; index--) pm_bignum_mul_add(result, 1000000000, chunks[index - 1]); + } else { + uint32_t level = pm_bsrll((uint64_t) nchunks - 1); + uint32_t split = (uint32_t) 1 << level; + + uint32_t *saved = scratch->cursor; + pm_bignum_t high = { .length = 0, .words = pm_bignum_scratch_take(scratch, pm_integer_chunks_words(nchunks - split)) }; + pm_integer_parse_decimal_divide_chunks(&high, nchunks - split, chunks + split, powers, scratch); + + assert(high.length + powers[level].length <= pm_integer_chunks_words(nchunks)); + pm_bignum_multiply(result, &high, &powers[level], scratch); + scratch->cursor = saved; + + pm_bignum_t low = { .length = 0, .words = pm_bignum_scratch_take(scratch, pm_integer_chunks_words(split)) }; + pm_integer_parse_decimal_divide_chunks(&low, split, chunks, powers, scratch); + pm_bignum_add(result, &low); + scratch->cursor = saved; + } +} + +/* + * Parse the digits of a decimal integer of more than + * PM_INTEGER_DIVIDE_THRESHOLD chunks of nine digits by divide and conquer. The + * digits are packed into chunks, the powers of ten that the splits need are + * computed by repeated squaring, and the result is converted straight into + * words allocated from the arena. Everything else lives in one block of + * scratch space from the heap, sized for the worst case: the chunks, the + * powers, and the halves and products of the deepest chain of splits, with + * Karatsuba's temporaries bounded by 12 times the longer factor's length. + */ +static void +pm_integer_parse_decimal_divide(pm_arena_t *arena, pm_integer_t *integer, uint32_t digits, const uint8_t *start, const uint8_t *end) { + uint32_t nchunks = (uint32_t) (((uint64_t) digits + 8) / 9); + uint32_t capacity = pm_integer_chunks_words(nchunks); + + pm_bignum_t result = { + .length = 0, + .words = (uint32_t *) pm_arena_alloc(arena, sizeof(uint32_t) * capacity, PRISM_ALIGNOF(uint32_t)) + }; + + /* The scratch space is about 12 times the number of chunks, which can + * exceed 32 bits, and on a 32-bit platform its size in bytes can exceed + * size_t. */ + uint64_t scratch_size = (uint64_t) nchunks + 12 * (uint64_t) capacity + 1024; + if (scratch_size > SIZE_MAX / sizeof(uint32_t)) PRISM_OOM(); + + uint32_t *allocation = (uint32_t *) xmalloc(sizeof(uint32_t) * (size_t) scratch_size); + if (allocation == NULL) PRISM_OOM(); + pm_bignum_scratch_t scratch = { .cursor = allocation, .end = allocation + (size_t) scratch_size }; + + /* Pack the digits into chunks of nine, least significant first. The most + * significant chunk takes the leftover digits so the rest are exactly + * nine. */ + uint32_t *chunks = pm_bignum_scratch_take(&scratch, nchunks); + uint32_t chunk_size = digits % 9 == 0 ? 9 : digits % 9; + uint32_t chunk_digits = 0; + uint32_t chunk_index = nchunks; + uint32_t chunk = 0; + + for (const uint8_t *cursor = start; cursor < end; cursor++) { + if (pm_integer_separator(*cursor)) continue; + chunk = chunk * 10 + pm_integer_parse_digit(*cursor); + + if (++chunk_digits == chunk_size) { + chunks[--chunk_index] = chunk; + chunk_size = 9; + chunk_digits = 0; + chunk = 0; + } + } + assert(chunk_index == 0); + + /* powers[level] is 10**(9 * 2**level), for every level a split uses. */ + pm_bignum_t powers[64]; + uint32_t level = 0; + powers[level].words = pm_bignum_scratch_take(&scratch, 1); + powers[level].words[0] = 1000000000; + powers[level].length = 1; + + for (uint32_t split = 2; split < nchunks; split *= 2) { + const pm_bignum_t *previous = &powers[level++]; + powers[level].words = pm_bignum_scratch_take(&scratch, 2 * previous->length); + pm_bignum_multiply(&powers[level], previous, previous, &scratch); + } + + pm_integer_parse_decimal_divide_chunks(&result, nchunks, chunks, powers, &scratch); + xfree_sized(allocation, sizeof(uint32_t) * (size_t) scratch_size); + + assert(result.length > 2 && result.length <= capacity); + pm_integer_from_bignum(arena, integer, &result, capacity); +} + /** * Parse the digits of a decimal integer that does not fit in two words. The * words are built in place in the arena: starting from zero, the number is @@ -129,21 +266,19 @@ pm_integer_parse_powof2(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, * nine. */ static void -pm_integer_parse_decimal(pm_arena_t *arena, pm_integer_t *integer, size_t digits, const uint8_t *start, const uint8_t *end) { +pm_integer_parse_decimal_schoolbook(pm_arena_t *arena, pm_integer_t *integer, uint32_t digits, const uint8_t *start, const uint8_t *end) { static const uint32_t powof10[10] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 }; - /* A number with d digits is below 10**d, so it needs at most - * d * log2(10) + 1 bits. 3402/1024 is just above log2(10), so this never - * undercounts, and the words that turn out to be unneeded are given back to - * the arena below. */ - size_t capacity = (digits * 3402 / 1024 + 32) / 32; + /* The words that turn out to be unneeded are given back to the arena + * below. */ + uint32_t capacity = pm_integer_decimal_words(digits); pm_bignum_t bignum = { .length = 0, .words = (uint32_t *) pm_arena_alloc(arena, sizeof(uint32_t) * capacity, PRISM_ALIGNOF(uint32_t)) }; - size_t chunk_size = digits % 9 == 0 ? 9 : digits % 9; - size_t chunk_digits = 0; + uint32_t chunk_size = digits % 9 == 0 ? 9 : digits % 9; + uint32_t chunk_digits = 0; uint32_t chunk = 0; for (const uint8_t *cursor = start; cursor < end; cursor++) { @@ -193,15 +328,17 @@ pm_integer_parse_digits(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, uint64_t digit = (uint64_t) pm_integer_parse_digit(*cursor); if (value > limit || (value == limit && digit > limit_digit)) { - size_t digits = 0; + uint32_t digits = 0; for (const uint8_t *scan = start; scan < end; scan++) { if (!pm_integer_separator(*scan)) digits++; } - if (base == 10) { - pm_integer_parse_decimal(arena, integer, digits, start, end); - } else { + if (base != 10) { pm_integer_parse_powof2(arena, integer, base, digits, start, end); + } else if (((uint64_t) digits + 8) / 9 > PM_INTEGER_DIVIDE_THRESHOLD) { + pm_integer_parse_decimal_divide(arena, integer, digits, start, end); + } else { + pm_integer_parse_decimal_schoolbook(arena, integer, digits, start, end); } return; @@ -264,14 +401,14 @@ pm_integer_parse_rational(pm_arena_t *arena, pm_integer_t *numerator, pm_integer * drop them before parsing. */ while (end > point + 1 && (end[-1] == '0' || end[-1] == '_')) end--; - size_t exponent = 0; + uint32_t exponent = 0; for (const uint8_t *cursor = point + 1; cursor < end; cursor++) { if (*cursor != '_') exponent++; } pm_integer_parse_digits(arena, numerator, 10, start, end); - size_t twos = 0; - size_t fives = 0; + uint32_t twos = 0; + uint32_t fives = 0; if (numerator->length > 0 && exponent > 0) { uint32_t length = numerator->length; @@ -280,8 +417,8 @@ pm_integer_parse_rational(pm_arena_t *arena, pm_integer_t *numerator, pm_integer .words = length <= 2 ? numerator->words.value : numerator->words.values }; - twos = pm_bignum_trailing_zeros(&bignum); - if (twos > exponent) twos = exponent; + uint64_t trailing = pm_bignum_trailing_zeros(&bignum); + twos = trailing > exponent ? exponent : (uint32_t) trailing; pm_bignum_shift_right(&bignum, twos); /* 5**13 is the largest power of five that fits in a word, so divide by @@ -299,9 +436,9 @@ pm_integer_parse_rational(pm_arena_t *arena, pm_integer_t *numerator, pm_integer /* The denominator is what is left of 10**exponent. 2378/1024 is just above * log2(5), so this capacity never undercounts. */ - size_t remaining_fives = exponent - fives; - size_t remaining_twos = exponent - twos; - size_t capacity = (remaining_fives * 2378 / 1024 + remaining_twos + 32) / 32; + uint32_t remaining_fives = exponent - fives; + uint32_t remaining_twos = exponent - twos; + uint32_t capacity = (uint32_t) (((uint64_t) remaining_fives * 2378 / 1024 + remaining_twos + 32) / 32); pm_bignum_t bignum = { .length = 1, diff --git a/test/prism/result/integer_parse_test.rb b/test/prism/result/integer_parse_test.rb index 7b5ce98bb6..8b7672c826 100644 --- a/test/prism/result/integer_parse_test.rb +++ b/test/prism/result/integer_parse_test.rb @@ -30,6 +30,9 @@ def test_integer_parse assert_integer_parse(num, "0o#{num.to_s(8)}") assert_integer_parse(num, "0d#{num.to_s(10)}") assert_integer_parse(num, "0x#{num.to_s(16)}") + + assert_integer_parse(7 ** 30_000) + assert_integer_parse(10 ** 5_000 - 1) end private From f13fe927e24646d64ad8ac851d06bd19be3a83d9 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 2 Nov 2026 11:43:22 -0500 Subject: [PATCH 09/14] Use divide-and-conquer for printing large integers --- include/prism/internal/bignum.h | 23 ++ src/bignum.c | 325 ++++++++++++++++++----- src/integer.c | 164 +++++++++++- test/prism/result/static_inspect_test.rb | 6 + 4 files changed, 444 insertions(+), 74 deletions(-) diff --git a/include/prism/internal/bignum.h b/include/prism/internal/bignum.h index fd2eaac4f3..d35e435f66 100644 --- a/include/prism/internal/bignum.h +++ b/include/prism/internal/bignum.h @@ -15,6 +15,16 @@ #include #include +/* + * The base of the words of a number. + */ +#define PM_BIGNUM_BINARY_BASE ((uint64_t) 1 << 32) + +/* + * The base of the words of a decimal number, each of which is nine digits. + */ +#define PM_BIGNUM_DECIMAL_BASE 1000000000 + /* * An arbitrary-precision unsigned integer. */ @@ -202,4 +212,17 @@ void pm_bignum_add(pm_bignum_t *left, const pm_bignum_t *right); */ void pm_bignum_multiply(pm_bignum_t *result, const pm_bignum_t *left, const pm_bignum_t *right, pm_bignum_scratch_t *scratch); +/* + * Add right into left, in place, in base 10**9. The storage must have room for + * the sum, which is at most one word longer than the longer of the two. + */ +void pm_bignum_decimal_add(pm_bignum_t *left, const pm_bignum_t *right); + +/* + * Multiply left by right into result, in base 10**9, whose storage has room + * for left->length + right->length words and overlaps neither. Long numbers + * use Karatsuba's method, which takes its temporaries from scratch. + */ +void pm_bignum_decimal_multiply(pm_bignum_t *result, const pm_bignum_t *left, const pm_bignum_t *right, pm_bignum_scratch_t *scratch); + #endif diff --git a/src/bignum.c b/src/bignum.c index 9d9954dd8c..c59f483e14 100644 --- a/src/bignum.c +++ b/src/bignum.c @@ -1,3 +1,4 @@ +#include "prism/compiler/force_inline.h" #include "prism/internal/bignum.h" #include @@ -12,51 +13,89 @@ #define PM_BIGNUM_KARATSUBA_THRESHOLD 24 /* - * Add right into left, in place. The sum must fit in left's length words. + * Add right into left, in place, in the given base, where left is at least as + * long. Returns the carry out of left's length words. Two words plus a carry + * are below twice the base, so each sum fits in a uint64_t and carries at most + * one into the next word. */ -static void -pm_bignum_words_add(uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { - uint64_t carry = 0; +static PRISM_FORCE_INLINE uint32_t +pm_bignum_base_words_add(uint64_t base, uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + uint32_t carry = 0; uint32_t index = 0; for (; index < right_length; index++) { - carry += (uint64_t) left[index] + right[index]; - left[index] = (uint32_t) carry; - carry >>= 32; + uint64_t sum = (uint64_t) left[index] + right[index] + carry; + carry = sum >= base; + left[index] = (uint32_t) (carry ? sum - base : sum); } - for (; carry != 0; index++) { - assert(index < left_length); - carry += left[index]; - left[index] = (uint32_t) carry; - carry >>= 32; + for (; carry != 0 && index < left_length; index++) { + uint64_t sum = (uint64_t) left[index] + 1; + carry = sum == base; + left[index] = (uint32_t) (carry ? 0 : sum); } + + return carry; } /* - * Subtract right from left, in place. Left must be at least right. A word minus - * a word minus a borrow wraps around to a value with its top bit set exactly - * when it is negative, so that bit is the next borrow. + * Subtract right from left, in place, in the given base. Left must be at least + * right. A word plus the base fits in a uint64_t, so borrowing does too. */ -static void -pm_bignum_words_subtract(uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { - uint64_t borrow = 0; +static PRISM_FORCE_INLINE void +pm_bignum_base_words_subtract(uint64_t base, uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + uint32_t borrow = 0; uint32_t index = 0; for (; index < right_length; index++) { - uint64_t difference = (uint64_t) left[index] - right[index] - borrow; - left[index] = (uint32_t) difference; - borrow = difference >> 63; + uint64_t subtrahend = (uint64_t) right[index] + borrow; + borrow = left[index] < subtrahend; + left[index] = (uint32_t) (borrow ? left[index] + base - subtrahend : left[index] - subtrahend); } for (; borrow != 0; index++) { assert(index < left_length); - uint64_t difference = (uint64_t) left[index] - borrow; - left[index] = (uint32_t) difference; - borrow = difference >> 63; + borrow = left[index] == 0; + left[index] = (uint32_t) (borrow ? base - 1 : left[index] - 1); } } +/* + * Add right into left, in place, in base 2**32, where left is at least as + * long. Returns the carry out of left's length words. + */ +static uint32_t +pm_bignum_words_add(uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + return pm_bignum_base_words_add(PM_BIGNUM_BINARY_BASE, left, left_length, right, right_length); +} + +/* + * Add right into left, in place, in base 10**9, where left is at least as + * long. Returns the carry out of left's length words. + */ +static uint32_t +pm_bignum_decimal_words_add(uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + return pm_bignum_base_words_add(PM_BIGNUM_DECIMAL_BASE, left, left_length, right, right_length); +} + +/* + * Subtract right from left, in place, in base 2**32. Left must be at least + * right. + */ +static void +pm_bignum_words_subtract(uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + pm_bignum_base_words_subtract(PM_BIGNUM_BINARY_BASE, left, left_length, right, right_length); +} + +/* + * Subtract right from left, in place, in base 10**9. Left must be at least + * right. + */ +static void +pm_bignum_decimal_words_subtract(uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + pm_bignum_base_words_subtract(PM_BIGNUM_DECIMAL_BASE, left, left_length, right, right_length); +} + /* * Multiply left by right into result, which has room for left_length + * right_length words and overlaps neither, using the schoolbook method. A word @@ -81,16 +120,132 @@ pm_bignum_words_multiply_schoolbook(uint32_t *result, const uint32_t *left, uint } /* - * Multiply left by right into result, which has room for left_length + - * right_length words and overlaps neither. Above PM_BIGNUM_KARATSUBA_THRESHOLD - * this uses Karatsuba's method: splitting each number into low and high halves - * at the same word, low * low and high * high go straight into the bottom and - * top of the result, and the middle term is (low + high) * (low + high) minus - * those two, which takes three multiplications of half the size instead of - * four. + * The number of products of two words in base 10**9 that can be added to a + * word before the sum could overflow a uint64_t: 18 * (10**9 - 1)**2 plus + * 10**9 - 1 is just below 1.8 * 10**19, while 2**64 is just above + * 1.84 * 10**19. + */ +#define PM_BIGNUM_DECIMAL_PRODUCTS 18 + +/* + * Multiply left by right into result, in base 10**9, which has room for + * left_length + right_length words and overlaps neither, using the schoolbook + * method. Dividing by 10**9 is what costs the most here, so each word of the + * result is computed in one go as the sum of the products of the pairs of + * words that land on it, which only divides once every + * PM_BIGNUM_DECIMAL_PRODUCTS products to keep the sum in a uint64_t, and once + * more to carry into the next word. */ static void -pm_bignum_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length, pm_bignum_scratch_t *scratch) { +pm_bignum_decimal_words_multiply_schoolbook(uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + if (left_length == 0 || right_length == 0) { + memset(result, 0, sizeof(uint32_t) * (left_length + right_length)); + return; + } + + /* The sum of every column is below its number of products times 10**18, + * and there are fewer than 2**29 products in a column, so the carry into + * the next column is below 2**29 * 10**9 and fits in a uint64_t. */ + uint64_t carry = 0; + + for (uint32_t index = 0; index < left_length + right_length - 1; index++) { + uint32_t left_start = index < right_length ? 0 : index - right_length + 1; + uint32_t left_end = index < left_length ? index + 1 : left_length; + + uint64_t low = 0; + uint64_t high = 0; + uint32_t products = 0; + + for (uint32_t left_index = left_start; left_index < left_end; left_index++) { + low += (uint64_t) left[left_index] * right[index - left_index]; + + if (++products == PM_BIGNUM_DECIMAL_PRODUCTS) { + high += low / PM_BIGNUM_DECIMAL_BASE; + low %= PM_BIGNUM_DECIMAL_BASE; + products = 0; + } + } + + low += carry % PM_BIGNUM_DECIMAL_BASE; + high += carry / PM_BIGNUM_DECIMAL_BASE; + + result[index] = (uint32_t) (low % PM_BIGNUM_DECIMAL_BASE); + carry = high + low / PM_BIGNUM_DECIMAL_BASE; + } + + /* The product is below 10**(9 * (left_length + right_length)), so what is + * left over is the top word. */ + assert(carry < PM_BIGNUM_DECIMAL_BASE); + result[left_length + right_length - 1] = (uint32_t) carry; +} + +static void pm_bignum_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length, pm_bignum_scratch_t *scratch); +static void pm_bignum_decimal_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length, pm_bignum_scratch_t *scratch); + +/* + * The helpers below take the base of the words, 2**32 or 10**9, as a + * parameter, but are always inlined into callers that pass a constant, so the + * choice is made at compile time and every call they make is direct. + */ + +/* + * Add right into left in the given base. Returns the carry out of left's + * length words. + */ +static PRISM_FORCE_INLINE uint32_t +pm_bignum_radix_add(uint64_t base, uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + return base == PM_BIGNUM_DECIMAL_BASE ? pm_bignum_decimal_words_add(left, left_length, right, right_length) : pm_bignum_words_add(left, left_length, right, right_length); +} + +/* + * Subtract right from left in the given base. + */ +static PRISM_FORCE_INLINE void +pm_bignum_radix_subtract(uint64_t base, uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + if (base == PM_BIGNUM_DECIMAL_BASE) { + pm_bignum_decimal_words_subtract(left, left_length, right, right_length); + } else { + pm_bignum_words_subtract(left, left_length, right, right_length); + } +} + +/* + * Multiply left by right with the schoolbook method in the given base. + */ +static PRISM_FORCE_INLINE void +pm_bignum_radix_multiply_schoolbook(uint64_t base, uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + if (base == PM_BIGNUM_DECIMAL_BASE) { + pm_bignum_decimal_words_multiply_schoolbook(result, left, left_length, right, right_length); + } else { + pm_bignum_words_multiply_schoolbook(result, left, left_length, right, right_length); + } +} + +/* + * Multiply left by right in the given base. + */ +static PRISM_FORCE_INLINE void +pm_bignum_radix_multiply(uint64_t base, uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length, pm_bignum_scratch_t *scratch) { + if (base == PM_BIGNUM_DECIMAL_BASE) { + pm_bignum_decimal_words_multiply(result, left, left_length, right, right_length, scratch); + } else { + pm_bignum_words_multiply(result, left, left_length, right, right_length, scratch); + } +} + +/* + * Multiply left by right into result, which has room for left_length + + * right_length words and overlaps neither, in the given base. Above + * PM_BIGNUM_KARATSUBA_THRESHOLD this uses Karatsuba's method: splitting each + * number into low and high halves at the same word, low * low and high * high + * go straight into the bottom and top of the result, and the middle term is + * (low + high) * (low + high) minus those two, which takes three + * multiplications of half the size instead of four. None of this depends on + * the base, only the additions, subtractions, and schoolbook multiplications + * that it is built from. + */ +static PRISM_FORCE_INLINE void +pm_bignum_radix_karatsuba(uint64_t base, uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length, pm_bignum_scratch_t *scratch) { if (left_length < right_length) { const uint32_t *words = left; left = right; @@ -102,7 +257,7 @@ pm_bignum_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_l } if (right_length < PM_BIGNUM_KARATSUBA_THRESHOLD) { - pm_bignum_words_multiply_schoolbook(result, left, left_length, right, right_length); + pm_bignum_radix_multiply_schoolbook(base, result, left, left_length, right, right_length); return; } @@ -117,8 +272,8 @@ pm_bignum_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_l for (uint32_t offset = 0; offset < left_length; offset += right_length) { uint32_t piece = left_length - offset < right_length ? left_length - offset : right_length; - pm_bignum_words_multiply(product, left + offset, piece, right, right_length, scratch); - pm_bignum_words_add(result + offset, left_length + right_length - offset, product, piece + right_length); + pm_bignum_radix_multiply(base, product, left + offset, piece, right, right_length, scratch); + pm_bignum_radix_add(base, result + offset, left_length + right_length - offset, product, piece + right_length); } scratch->cursor = saved; @@ -129,60 +284,87 @@ pm_bignum_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_l uint32_t left_high_length = left_length - half; uint32_t right_high_length = right_length - half; - pm_bignum_words_multiply(result, left, half, right, half, scratch); - pm_bignum_words_multiply(result + 2 * half, left + half, left_high_length, right + half, right_high_length, scratch); + pm_bignum_radix_multiply(base, result, left, half, right, half, scratch); + pm_bignum_radix_multiply(base, result + 2 * half, left + half, left_high_length, right + half, right_high_length, scratch); /* Each high half is at least as long as the low half, so adding the low * half into a copy of the high half needs at most one more word. */ uint32_t *left_sum = pm_bignum_scratch_take(scratch, left_high_length + 1); memcpy(left_sum, left + half, sizeof(uint32_t) * left_high_length); left_sum[left_high_length] = 0; - pm_bignum_words_add(left_sum, left_high_length + 1, left, half); + pm_bignum_radix_add(base, left_sum, left_high_length + 1, left, half); uint32_t *right_sum = pm_bignum_scratch_take(scratch, right_high_length + 1); memcpy(right_sum, right + half, sizeof(uint32_t) * right_high_length); right_sum[right_high_length] = 0; - pm_bignum_words_add(right_sum, right_high_length + 1, right, half); + pm_bignum_radix_add(base, right_sum, right_high_length + 1, right, half); uint32_t middle_length = left_high_length + right_high_length + 2; uint32_t *middle = pm_bignum_scratch_take(scratch, middle_length); - pm_bignum_words_multiply(middle, left_sum, left_high_length + 1, right_sum, right_high_length + 1, scratch); - pm_bignum_words_subtract(middle, middle_length, result, 2 * half); - pm_bignum_words_subtract(middle, middle_length, result + 2 * half, left_high_length + right_high_length); + pm_bignum_radix_multiply(base, middle, left_sum, left_high_length + 1, right_sum, right_high_length + 1, scratch); + pm_bignum_radix_subtract(base, middle, middle_length, result, 2 * half); + pm_bignum_radix_subtract(base, middle, middle_length, result + 2 * half, left_high_length + right_high_length); while (middle_length > 0 && middle[middle_length - 1] == 0) middle_length--; - pm_bignum_words_add(result + half, left_length + right_length - half, middle, middle_length); + pm_bignum_radix_add(base, result + half, left_length + right_length - half, middle, middle_length); scratch->cursor = saved; } /* - * Add right into left, in place. The storage must have room for the sum, which - * is at most one word longer than the longer of the two. + * Multiply left by right into result in base 2**32, which has room for + * left_length + right_length words and overlaps neither. */ -void -pm_bignum_add(pm_bignum_t *left, const pm_bignum_t *right) { +static void +pm_bignum_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length, pm_bignum_scratch_t *scratch) { + pm_bignum_radix_karatsuba(PM_BIGNUM_BINARY_BASE, result, left, left_length, right, right_length, scratch); +} + +/* + * Multiply left by right into result in base 10**9, which has room for + * left_length + right_length words and overlaps neither. + */ +static void +pm_bignum_decimal_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length, pm_bignum_scratch_t *scratch) { + pm_bignum_radix_karatsuba(PM_BIGNUM_DECIMAL_BASE, result, left, left_length, right, right_length, scratch); +} + +/* + * Add right into left, in place, in the given base. Left is zero-extended to + * the length of right if it is shorter, and a carry out of the top becomes a + * new top word. + */ +static PRISM_FORCE_INLINE void +pm_bignum_radix_add_bignum(uint64_t base, pm_bignum_t *left, const pm_bignum_t *right) { if (left->length < right->length) { memset(left->words + left->length, 0, sizeof(uint32_t) * (right->length - left->length)); left->length = right->length; } - uint64_t carry = 0; - uint32_t index = 0; + uint32_t carry = pm_bignum_radix_add(base, left->words, left->length, right->words, right->length); + if (carry != 0) left->words[left->length++] = carry; +} - for (; index < right->length; index++) { - carry += (uint64_t) left->words[index] + right->words[index]; - left->words[index] = (uint32_t) carry; - carry >>= 32; - } +/* + * Multiply left by right into result, in the given base, and trim the zero + * words off the top of the product. + */ +static PRISM_FORCE_INLINE void +pm_bignum_radix_multiply_bignum(uint64_t base, pm_bignum_t *result, const pm_bignum_t *left, const pm_bignum_t *right, pm_bignum_scratch_t *scratch) { + uint32_t length = left->length + right->length; + pm_bignum_radix_multiply(base, result->words, left->words, left->length, right->words, right->length, scratch); - for (; carry != 0 && index < left->length; index++) { - carry += left->words[index]; - left->words[index] = (uint32_t) carry; - carry >>= 32; - } + while (length > 0 && result->words[length - 1] == 0) length--; + result->length = length; +} - if (carry != 0) left->words[left->length++] = (uint32_t) carry; +/* + * Add right into left, in place. The storage must have room for the sum, which + * is at most one word longer than the longer of the two. + */ +void +pm_bignum_add(pm_bignum_t *left, const pm_bignum_t *right) { + pm_bignum_radix_add_bignum(PM_BIGNUM_BINARY_BASE, left, right); } /* @@ -192,9 +374,24 @@ pm_bignum_add(pm_bignum_t *left, const pm_bignum_t *right) { */ void pm_bignum_multiply(pm_bignum_t *result, const pm_bignum_t *left, const pm_bignum_t *right, pm_bignum_scratch_t *scratch) { - uint32_t length = left->length + right->length; - pm_bignum_words_multiply(result->words, left->words, left->length, right->words, right->length, scratch); + pm_bignum_radix_multiply_bignum(PM_BIGNUM_BINARY_BASE, result, left, right, scratch); +} - while (length > 0 && result->words[length - 1] == 0) length--; - result->length = length; +/* + * Add right into left, in place, in base 10**9. The storage must have room for + * the sum, which is at most one word longer than the longer of the two. + */ +void +pm_bignum_decimal_add(pm_bignum_t *left, const pm_bignum_t *right) { + pm_bignum_radix_add_bignum(PM_BIGNUM_DECIMAL_BASE, left, right); +} + +/* + * Multiply left by right into result, in base 10**9, whose storage has room + * for left->length + right->length words and overlaps neither. Long numbers + * use Karatsuba's method, which takes its temporaries from scratch. + */ +void +pm_bignum_decimal_multiply(pm_bignum_t *result, const pm_bignum_t *left, const pm_bignum_t *right, pm_bignum_scratch_t *scratch) { + pm_bignum_radix_multiply_bignum(PM_BIGNUM_DECIMAL_BASE, result, left, right, scratch); } diff --git a/src/integer.c b/src/integer.c index 2f964d3af6..b689332f53 100644 --- a/src/integer.c +++ b/src/integer.c @@ -22,6 +22,30 @@ #include #include +/* + * When parsing long decimal digits, we switch to a divide-and-conquer approach + * once it gets really long because we start to see the impact of an O(n^1.58) + * vs an O(n^2) algorithm. + */ +#define PM_INTEGER_DIVIDE_THRESHOLD 224 + +/* + * When printing integers of many words, we switch to a divide-and-conquer + * approach for the same reason as parsing: the repeated division by 10**9 is + * O(n^2), while the divide and conquer is O(n^1.58). Below this many words, the + * scratch space and the powers of two that the divide and conquer needs cost + * more than it saves. + */ +#define PM_INTEGER_STRING_THRESHOLD 48 + +/* + * Within the divide and conquer, pieces of at most this many words are + * converted by repeated division by 10**9. This is lower than + * PM_INTEGER_STRING_THRESHOLD because the pieces share the scratch space and + * the powers of two, so they do not pay for them again. + */ +#define PM_INTEGER_STRING_LEAF 8 + /** * The values of a hexadecimal digit, where the index is the ASCII character. * Note that there's an odd exception here where _ is mapped to 0. This is @@ -122,13 +146,6 @@ pm_integer_parse_powof2(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, *integer = (pm_integer_t) { .length = length, .words = { .values = words } }; } -/* - * When parsing long decimal digits, we switch to a divide-and-conquer approach - * once it gets really long because we start to see the impact of an O(n^1.58) - * vs an O(n^2) algorithm. - */ -#define PM_INTEGER_DIVIDE_THRESHOLD 224 - /* * The number of words needed to hold a decimal integer with the given number * of digits. A number with d digits is below 10**d, so it needs at most: @@ -166,7 +183,7 @@ static void pm_integer_parse_decimal_divide_chunks(pm_bignum_t *result, uint32_t nchunks, const uint32_t *chunks, const pm_bignum_t *powers, pm_bignum_scratch_t *scratch) { if (nchunks <= PM_INTEGER_DIVIDE_THRESHOLD) { result->length = 0; - for (uint32_t index = nchunks; index > 0; index--) pm_bignum_mul_add(result, 1000000000, chunks[index - 1]); + for (uint32_t index = nchunks; index > 0; index--) pm_bignum_mul_add(result, PM_BIGNUM_DECIMAL_BASE, chunks[index - 1]); } else { uint32_t level = pm_bsrll((uint64_t) nchunks - 1); uint32_t split = (uint32_t) 1 << level; @@ -242,7 +259,7 @@ pm_integer_parse_decimal_divide(pm_arena_t *arena, pm_integer_t *integer, uint32 pm_bignum_t powers[64]; uint32_t level = 0; powers[level].words = pm_bignum_scratch_take(&scratch, 1); - powers[level].words[0] = 1000000000; + powers[level].words[0] = PM_BIGNUM_DECIMAL_BASE; powers[level].length = 1; for (uint32_t split = 2; split < nchunks; split *= 2) { @@ -480,6 +497,128 @@ pm_integer_compare(const pm_integer_t *left, const pm_integer_t *right) { return pm_bignum_compare(&left_bignum, &right_bignum) * negative; } +/* + * The number of words of storage needed to convert an integer of length words + * into base 10**9. The integer is below 2**(32 * length), so it needs at most: + * + * 32 * length * log10(2) / 9 + * + * words in base 10**9, rounded up. To find 32 * log10(2) / 9, we approximate it + * as 1097/1024, which is slightly greater than the actual value so it does not + * undercount. As in pm_integer_chunks_words, the conversion multiplies the + * high half by a power of two into this storage, which writes the sum of its + * factors' lengths, and each factor's length rounds up on its own, so this is + * one word more than the value needs. + */ +static PRISM_INLINE uint32_t +pm_integer_string_words(uint32_t length) { + return (uint32_t) ((uint64_t) length * 1097 / 1024 + 2); +} + +/* + * Convert length words of a bignum into base 10**9. The result's storage must + * have room for pm_integer_string_words(length) words. Up to + * PM_INTEGER_STRING_LEAF words, a copy of the words is divided by 10**9 + * until nothing is left, and each remainder is the next word of the result. + * Past it, the words split below the largest power of two less than length, at + * 2**level words, so the value is high * 2**(32 * 2**level) + low with that + * power of two already in base 10**9 in powers[level], and both halves convert + * the same way. + */ +static void +pm_integer_string_convert(pm_bignum_t *result, const uint32_t *words, uint32_t length, const pm_bignum_t *powers, pm_bignum_scratch_t *scratch) { + /* The low half of a split can have leading zero words. */ + while (length > 0 && words[length - 1] == 0) length--; + uint32_t *saved = scratch->cursor; + + if (length <= PM_INTEGER_STRING_LEAF) { + pm_bignum_t bignum = { .length = length, .words = pm_bignum_scratch_take(scratch, length) }; + memcpy(bignum.words, words, sizeof(uint32_t) * length); + + result->length = 0; + while (bignum.length > 0) result->words[result->length++] = pm_bignum_div(&bignum, PM_BIGNUM_DECIMAL_BASE); + } else { + uint32_t level = pm_bsrll((uint64_t) length - 1); + uint32_t split = (uint32_t) 1 << level; + + pm_bignum_t high = { .length = 0, .words = pm_bignum_scratch_take(scratch, pm_integer_string_words(length - split)) }; + pm_integer_string_convert(&high, words + split, length - split, powers, scratch); + + assert(high.length + powers[level].length <= pm_integer_string_words(length)); + pm_bignum_decimal_multiply(result, &high, &powers[level], scratch); + scratch->cursor = saved; + + pm_bignum_t low = { .length = 0, .words = pm_bignum_scratch_take(scratch, pm_integer_string_words(split)) }; + pm_integer_string_convert(&low, words, split, powers, scratch); + pm_bignum_decimal_add(result, &low); + } + + scratch->cursor = saved; +} + +/* + * Convert the words of an integer of more than PM_INTEGER_STRING_THRESHOLD + * words to a decimal string by divide and conquer. The powers of two that the + * splits need are computed in base 10**9 by repeated squaring, and the integer + * is converted into base 10**9, each word of which is nine digits. Everything + * lives in one block of scratch space from the heap, sized for the worst case: + * the result, the powers, and the halves and products of the deepest chain of + * splits, with Karatsuba's temporaries bounded by 12 times the longer factor's + * length. + */ +static void +pm_integer_string_divide(pm_buffer_t *buffer, const uint32_t *words, uint32_t length) { + uint32_t capacity = pm_integer_string_words(length); + + /* The scratch space is about 13 times the number of words, which can + * exceed 32 bits, and on a 32-bit platform its size in bytes can exceed + * size_t. */ + uint64_t scratch_size = 12 * (uint64_t) capacity + 1024; + if (scratch_size > SIZE_MAX / sizeof(uint32_t)) PRISM_OOM(); + + uint32_t *allocation = (uint32_t *) xmalloc(sizeof(uint32_t) * (size_t) scratch_size); + if (allocation == NULL) PRISM_OOM(); + pm_bignum_scratch_t scratch = { .cursor = allocation, .end = allocation + (size_t) scratch_size }; + + pm_bignum_t result = { .length = 0, .words = pm_bignum_scratch_take(&scratch, capacity) }; + + /* powers[level] is 2**(32 * 2**level) in base 10**9, for every level a + * split uses. */ + pm_bignum_t powers[64]; + uint32_t level = 0; + powers[level].words = pm_bignum_scratch_take(&scratch, 2); + powers[level].words[0] = (uint32_t) (PM_BIGNUM_BINARY_BASE % PM_BIGNUM_DECIMAL_BASE); + powers[level].words[1] = (uint32_t) (PM_BIGNUM_BINARY_BASE / PM_BIGNUM_DECIMAL_BASE); + powers[level].length = 2; + + for (uint32_t split = 2; split < length; split *= 2) { + const pm_bignum_t *previous = &powers[level++]; + powers[level].words = pm_bignum_scratch_take(&scratch, 2 * previous->length); + pm_bignum_decimal_multiply(&powers[level], previous, previous, &scratch); + } + + pm_integer_string_convert(&result, words, length, powers, &scratch); + assert(result.length > 0 && result.length < capacity); + + /* Every word but the most significant is exactly nine digits, padded with + * zeros. */ + pm_buffer_append_format(buffer, "%" PRIu32, result.words[result.length - 1]); + + for (uint32_t index = result.length - 1; index > 0; index--) { + uint32_t word = result.words[index - 1]; + char digits[9]; + + for (size_t digit = 9; digit > 0; digit--) { + digits[digit - 1] = (char) ('0' + word % 10); + word /= 10; + } + + pm_buffer_append_string(buffer, digits, 9); + } + + xfree_sized(allocation, sizeof(uint32_t) * (size_t) scratch_size); +} + /** * Convert an integer to a decimal string. */ @@ -497,6 +636,11 @@ pm_integer_string(pm_buffer_t *buffer, const pm_integer_t *integer) { return; } + if (integer->length > PM_INTEGER_STRING_THRESHOLD) { + pm_integer_string_divide(buffer, integer->words.values, integer->length); + return; + } + /* Dividing by 10**9 yields the next nine digits from the remainder, least * significant first, so the digits are written from the end of the buffer * toward the front. The division is destructive, so it works on a copy. */ @@ -518,7 +662,7 @@ pm_integer_string(pm_buffer_t *buffer, const pm_integer_t *integer) { size_t offset = capacity; while (bignum.length > 0) { - uint32_t remainder = pm_bignum_div(&bignum, 1000000000); + uint32_t remainder = pm_bignum_div(&bignum, PM_BIGNUM_DECIMAL_BASE); /* Every chunk but the most significant is exactly nine digits, padded * with zeros. The most significant chunk is nonzero and stops at its diff --git a/test/prism/result/static_inspect_test.rb b/test/prism/result/static_inspect_test.rb index cf8cef3298..51752177e4 100644 --- a/test/prism/result/static_inspect_test.rb +++ b/test/prism/result/static_inspect_test.rb @@ -30,6 +30,12 @@ def test_imaginary def test_integer assert_equal "1000", static_inspect("1_0_0_0") assert_equal "10000000000000000000000000000", static_inspect("1_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0") + + num = 7 ** 30_000 + assert_equal num.to_s, static_inspect("0x#{num.to_s(16)}") + + num = 10 ** 5_000 - 1 + assert_equal num.to_s, static_inspect("0x#{num.to_s(16)}") end def test_nil From 8b62be88c0f780deb086bf9ebd5063b2437481cc Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 2 Nov 2026 11:57:07 -0500 Subject: [PATCH 10/14] Some cleanup and conveniences around integers --- include/prism/integer.h | 16 +++++++- include/prism/internal/bignum.h | 11 ++++++ include/prism/internal/bit.h | 4 +- include/prism/internal/parser.h | 9 +++-- src/bignum.c | 48 ++++++++++++------------ src/integer.c | 34 +++++++---------- src/static_literals.c | 4 +- templates/ext/prism/api_node.c.erb | 2 +- test/prism/result/static_inspect_test.rb | 15 ++++++++ 9 files changed, 88 insertions(+), 55 deletions(-) diff --git a/include/prism/integer.h b/include/prism/integer.h index 132dd26c6a..a6ffc67b05 100644 --- a/include/prism/integer.h +++ b/include/prism/integer.h @@ -29,8 +29,8 @@ typedef struct { bool negative; /** - * The words of the magnitude. Read them through pm_integer_words rather - * than directly. + * The words of the magnitude. Read them through pm_integer_words or + * pm_integer_uint64_value rather than directly. */ union { /** @@ -64,4 +64,16 @@ pm_integer_words(const pm_integer_t *integer, uint32_t *length) { } } +/** + * Return the magnitude of an integer whose length is at most 2, which always + * fits in a uint64_t. + * + * @param integer The integer to read, whose length must be at most 2. + * @returns The magnitude of the integer. + */ +static PRISM_INLINE uint64_t +pm_integer_uint64_value(const pm_integer_t *integer) { + return ((uint64_t) integer->words.value[1] << 32) | integer->words.value[0]; +} + #endif diff --git a/include/prism/internal/bignum.h b/include/prism/internal/bignum.h index d35e435f66..d344271cb0 100644 --- a/include/prism/internal/bignum.h +++ b/include/prism/internal/bignum.h @@ -175,6 +175,17 @@ pm_bignum_compare(const pm_bignum_t *left, const pm_bignum_t *right) { return 0; } +/* + * Return the value of a number of at most two words. Words past the length may + * hold stale values after an in-place operation, so only the words in use are + * read. + */ +static PRISM_INLINE uint64_t +pm_bignum_uint64(const pm_bignum_t *bignum) { + assert(bignum->length <= 2); + return (bignum->length > 0 ? (uint64_t) bignum->words[0] : 0) | (bignum->length > 1 ? (uint64_t) bignum->words[1] << 32 : 0); +} + /* * Temporary words for the operations that need them, taken and given back in * stack order: an operation saves the cursor, takes what it needs, and diff --git a/include/prism/internal/bit.h b/include/prism/internal/bit.h index 9f7d259de9..39181bc4c4 100644 --- a/include/prism/internal/bit.h +++ b/include/prism/internal/bit.h @@ -7,8 +7,8 @@ * Count trailing zero bits in a 64-bit value, and find the index of the * highest set bit in one (the result of the x86 bsr instruction). pm_ctzll is * used by SWAR identifier scanning to find the first non-matching byte in a - * word, and pm_bsrll by integer parsing to find the largest power of two below - * a count. + * word, and pm_bsrll by integer parsing and printing to find the largest power + * of two below a count. * * Precondition: v must be nonzero. The results are undefined when v == 0 * (matching the behavior of __builtin_ctzll, __builtin_clzll, diff --git a/include/prism/internal/parser.h b/include/prism/internal/parser.h index c3016d7c46..b9978ee88a 100644 --- a/include/prism/internal/parser.h +++ b/include/prism/internal/parser.h @@ -812,10 +812,11 @@ struct pm_parser_t { pm_node_flags_t base; /* - * When lexing a decimal integer that fits in a uint64_t, or a plain 0, - * we compute the value during lexing to avoid re-scanning the digits - * during parsing. If start is set, this holds the result for the - * token that begins there and pm_integer_parse can be skipped. + * When lexing a decimal integer of at most 19 digits, which always + * fits in a uint64_t, or a plain 0, we compute the value during + * lexing to avoid re-scanning the digits during parsing. If start is + * set, this holds the result for the token that begins there and + * pm_integer_parse can be skipped. */ uint64_t value; diff --git a/src/bignum.c b/src/bignum.c index c59f483e14..af3e1fcab6 100644 --- a/src/bignum.c +++ b/src/bignum.c @@ -6,13 +6,13 @@ #include #include -/* +/** * Multiplications where the shorter number has fewer words than this use the * schoolbook method, which is faster than Karatsuba's at small sizes. */ #define PM_BIGNUM_KARATSUBA_THRESHOLD 24 -/* +/** * Add right into left, in place, in the given base, where left is at least as * long. Returns the carry out of left's length words. Two words plus a carry * are below twice the base, so each sum fits in a uint64_t and carries at most @@ -38,7 +38,7 @@ pm_bignum_base_words_add(uint64_t base, uint32_t *left, uint32_t left_length, co return carry; } -/* +/** * Subtract right from left, in place, in the given base. Left must be at least * right. A word plus the base fits in a uint64_t, so borrowing does too. */ @@ -60,7 +60,7 @@ pm_bignum_base_words_subtract(uint64_t base, uint32_t *left, uint32_t left_lengt } } -/* +/** * Add right into left, in place, in base 2**32, where left is at least as * long. Returns the carry out of left's length words. */ @@ -69,7 +69,7 @@ pm_bignum_words_add(uint32_t *left, uint32_t left_length, const uint32_t *right, return pm_bignum_base_words_add(PM_BIGNUM_BINARY_BASE, left, left_length, right, right_length); } -/* +/** * Add right into left, in place, in base 10**9, where left is at least as * long. Returns the carry out of left's length words. */ @@ -78,7 +78,7 @@ pm_bignum_decimal_words_add(uint32_t *left, uint32_t left_length, const uint32_t return pm_bignum_base_words_add(PM_BIGNUM_DECIMAL_BASE, left, left_length, right, right_length); } -/* +/** * Subtract right from left, in place, in base 2**32. Left must be at least * right. */ @@ -87,7 +87,7 @@ pm_bignum_words_subtract(uint32_t *left, uint32_t left_length, const uint32_t *r pm_bignum_base_words_subtract(PM_BIGNUM_BINARY_BASE, left, left_length, right, right_length); } -/* +/** * Subtract right from left, in place, in base 10**9. Left must be at least * right. */ @@ -96,7 +96,7 @@ pm_bignum_decimal_words_subtract(uint32_t *left, uint32_t left_length, const uin pm_bignum_base_words_subtract(PM_BIGNUM_DECIMAL_BASE, left, left_length, right, right_length); } -/* +/** * Multiply left by right into result, which has room for left_length + * right_length words and overlaps neither, using the schoolbook method. A word * times a word plus two words is at most 2**64 - 1, so each step fits in a @@ -119,7 +119,7 @@ pm_bignum_words_multiply_schoolbook(uint32_t *result, const uint32_t *left, uint } } -/* +/** * The number of products of two words in base 10**9 that can be added to a * word before the sum could overflow a uint64_t: 18 * (10**9 - 1)**2 plus * 10**9 - 1 is just below 1.8 * 10**19, while 2**64 is just above @@ -127,7 +127,7 @@ pm_bignum_words_multiply_schoolbook(uint32_t *result, const uint32_t *left, uint */ #define PM_BIGNUM_DECIMAL_PRODUCTS 18 -/* +/** * Multiply left by right into result, in base 10**9, which has room for * left_length + right_length words and overlaps neither, using the schoolbook * method. Dividing by 10**9 is what costs the most here, so each word of the @@ -182,13 +182,13 @@ pm_bignum_decimal_words_multiply_schoolbook(uint32_t *result, const uint32_t *le static void pm_bignum_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length, pm_bignum_scratch_t *scratch); static void pm_bignum_decimal_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length, pm_bignum_scratch_t *scratch); -/* +/** * The helpers below take the base of the words, 2**32 or 10**9, as a * parameter, but are always inlined into callers that pass a constant, so the * choice is made at compile time and every call they make is direct. */ -/* +/** * Add right into left in the given base. Returns the carry out of left's * length words. */ @@ -197,7 +197,7 @@ pm_bignum_radix_add(uint64_t base, uint32_t *left, uint32_t left_length, const u return base == PM_BIGNUM_DECIMAL_BASE ? pm_bignum_decimal_words_add(left, left_length, right, right_length) : pm_bignum_words_add(left, left_length, right, right_length); } -/* +/** * Subtract right from left in the given base. */ static PRISM_FORCE_INLINE void @@ -209,7 +209,7 @@ pm_bignum_radix_subtract(uint64_t base, uint32_t *left, uint32_t left_length, co } } -/* +/** * Multiply left by right with the schoolbook method in the given base. */ static PRISM_FORCE_INLINE void @@ -221,7 +221,7 @@ pm_bignum_radix_multiply_schoolbook(uint64_t base, uint32_t *result, const uint3 } } -/* +/** * Multiply left by right in the given base. */ static PRISM_FORCE_INLINE void @@ -233,7 +233,7 @@ pm_bignum_radix_multiply(uint64_t base, uint32_t *result, const uint32_t *left, } } -/* +/** * Multiply left by right into result, which has room for left_length + * right_length words and overlaps neither, in the given base. Above * PM_BIGNUM_KARATSUBA_THRESHOLD this uses Karatsuba's method: splitting each @@ -311,7 +311,7 @@ pm_bignum_radix_karatsuba(uint64_t base, uint32_t *result, const uint32_t *left, scratch->cursor = saved; } -/* +/** * Multiply left by right into result in base 2**32, which has room for * left_length + right_length words and overlaps neither. */ @@ -320,7 +320,7 @@ pm_bignum_words_multiply(uint32_t *result, const uint32_t *left, uint32_t left_l pm_bignum_radix_karatsuba(PM_BIGNUM_BINARY_BASE, result, left, left_length, right, right_length, scratch); } -/* +/** * Multiply left by right into result in base 10**9, which has room for * left_length + right_length words and overlaps neither. */ @@ -329,7 +329,7 @@ pm_bignum_decimal_words_multiply(uint32_t *result, const uint32_t *left, uint32_ pm_bignum_radix_karatsuba(PM_BIGNUM_DECIMAL_BASE, result, left, left_length, right, right_length, scratch); } -/* +/** * Add right into left, in place, in the given base. Left is zero-extended to * the length of right if it is shorter, and a carry out of the top becomes a * new top word. @@ -345,7 +345,7 @@ pm_bignum_radix_add_bignum(uint64_t base, pm_bignum_t *left, const pm_bignum_t * if (carry != 0) left->words[left->length++] = carry; } -/* +/** * Multiply left by right into result, in the given base, and trim the zero * words off the top of the product. */ @@ -358,7 +358,7 @@ pm_bignum_radix_multiply_bignum(uint64_t base, pm_bignum_t *result, const pm_big result->length = length; } -/* +/** * Add right into left, in place. The storage must have room for the sum, which * is at most one word longer than the longer of the two. */ @@ -367,7 +367,7 @@ pm_bignum_add(pm_bignum_t *left, const pm_bignum_t *right) { pm_bignum_radix_add_bignum(PM_BIGNUM_BINARY_BASE, left, right); } -/* +/** * Multiply left by right into result, whose storage has room for * left->length + right->length words and overlaps neither. Long numbers use * Karatsuba's method, which takes its temporaries from scratch. @@ -377,7 +377,7 @@ pm_bignum_multiply(pm_bignum_t *result, const pm_bignum_t *left, const pm_bignum pm_bignum_radix_multiply_bignum(PM_BIGNUM_BINARY_BASE, result, left, right, scratch); } -/* +/** * Add right into left, in place, in base 10**9. The storage must have room for * the sum, which is at most one word longer than the longer of the two. */ @@ -386,7 +386,7 @@ pm_bignum_decimal_add(pm_bignum_t *left, const pm_bignum_t *right) { pm_bignum_radix_add_bignum(PM_BIGNUM_DECIMAL_BASE, left, right); } -/* +/** * Multiply left by right into result, in base 10**9, whose storage has room * for left->length + right->length words and overlaps neither. Long numbers * use Karatsuba's method, which takes its temporaries from scratch. diff --git a/src/integer.c b/src/integer.c index b689332f53..bf0e646a04 100644 --- a/src/integer.c +++ b/src/integer.c @@ -22,14 +22,14 @@ #include #include -/* +/** * When parsing long decimal digits, we switch to a divide-and-conquer approach * once it gets really long because we start to see the impact of an O(n^1.58) * vs an O(n^2) algorithm. */ #define PM_INTEGER_DIVIDE_THRESHOLD 224 -/* +/** * When printing integers of many words, we switch to a divide-and-conquer * approach for the same reason as parsing: the repeated division by 10**9 is * O(n^2), while the divide and conquer is O(n^1.58). Below this many words, the @@ -38,7 +38,7 @@ */ #define PM_INTEGER_STRING_THRESHOLD 48 -/* +/** * Within the divide and conquer, pieces of at most this many words are * converted by repeated division by 10**9. This is lower than * PM_INTEGER_STRING_THRESHOLD because the pieces share the scratch space and @@ -48,10 +48,6 @@ /** * The values of a hexadecimal digit, where the index is the ASCII character. - * Note that there's an odd exception here where _ is mapped to 0. This is - * because it's possible for us to end up trying to parse a number that has - * already had an error attached to it, and we want to provide _something_ to - * the user. */ static const int8_t pm_integer_parse_digit_values[256] = { /* 0 1 2 3 4 5 6 7 8 9 A B C D E F */ @@ -60,7 +56,7 @@ static const int8_t pm_integer_parse_digit_values[256] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 2x */ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, /* 3x */ -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 4x */ - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, /* 5x */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 5x */ -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 6x */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 7x */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 8x */ @@ -103,7 +99,7 @@ pm_integer_separator(uint8_t character) { static void pm_integer_from_bignum(pm_arena_t *arena, pm_integer_t *integer, const pm_bignum_t *bignum, uint32_t capacity) { if (bignum->length <= 2) { - uint64_t value = (bignum->length > 0 ? (uint64_t) bignum->words[0] : 0) | (bignum->length > 1 ? (uint64_t) bignum->words[1] << 32 : 0); + uint64_t value = pm_bignum_uint64(bignum); pm_arena_shrink(arena, bignum->words, sizeof(uint32_t) * capacity, 0); *integer = pm_integer_uint64(value); } else { @@ -146,7 +142,7 @@ pm_integer_parse_powof2(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, *integer = (pm_integer_t) { .length = length, .words = { .values = words } }; } -/* +/** * The number of words needed to hold a decimal integer with the given number * of digits. A number with d digits is below 10**d, so it needs at most: * @@ -161,7 +157,7 @@ pm_integer_decimal_words(uint64_t digits) { return (uint32_t) ((digits * 3402 / 1024) / 32 + 1); } -/* +/** * The number of words of storage needed to convert nchunks chunks of nine * decimal digits, which is one word more than the value needs. The conversion * multiplies the high half by a power of ten into this storage, and @@ -174,7 +170,7 @@ pm_integer_chunks_words(uint32_t nchunks) { return pm_integer_decimal_words((uint64_t) nchunks * 9) + 1; } -/* +/** * Convert nchunks of nine decimal digits into a bignum. Do this by effectively * converting the base of the number from 10**9 to 2**32. The result's storage * must have room for pm_integer_chunks_words(nchunks) words. @@ -203,7 +199,7 @@ pm_integer_parse_decimal_divide_chunks(pm_bignum_t *result, uint32_t nchunks, co } } -/* +/** * Parse the digits of a decimal integer of more than * PM_INTEGER_DIVIDE_THRESHOLD chunks of nine digits by divide and conquer. The * digits are packed into chunks, the powers of ten that the splits need are @@ -444,8 +440,7 @@ pm_integer_parse_rational(pm_arena_t *arena, pm_integer_t *numerator, pm_integer while (fives < exponent && pm_bignum_div_exact(&bignum, 5)) fives++; if (length <= 2) { - uint64_t value = (bignum.length > 0 ? (uint64_t) bignum.words[0] : 0) | (bignum.length > 1 ? (uint64_t) bignum.words[1] << 32 : 0); - *numerator = pm_integer_uint64(value); + *numerator = pm_integer_uint64(pm_bignum_uint64(&bignum)); } else { pm_integer_from_bignum(arena, numerator, &bignum, length); } @@ -497,7 +492,7 @@ pm_integer_compare(const pm_integer_t *left, const pm_integer_t *right) { return pm_bignum_compare(&left_bignum, &right_bignum) * negative; } -/* +/** * The number of words of storage needed to convert an integer of length words * into base 10**9. The integer is below 2**(32 * length), so it needs at most: * @@ -515,7 +510,7 @@ pm_integer_string_words(uint32_t length) { return (uint32_t) ((uint64_t) length * 1097 / 1024 + 2); } -/* +/** * Convert length words of a bignum into base 10**9. The result's storage must * have room for pm_integer_string_words(length) words. Up to * PM_INTEGER_STRING_LEAF words, a copy of the words is divided by 10**9 @@ -556,7 +551,7 @@ pm_integer_string_convert(pm_bignum_t *result, const uint32_t *words, uint32_t l scratch->cursor = saved; } -/* +/** * Convert the words of an integer of more than PM_INTEGER_STRING_THRESHOLD * words to a decimal string by divide and conquer. The powers of two that the * splits need are computed in base 10**9 by repeated squaring, and the integer @@ -631,8 +626,7 @@ pm_integer_string(pm_buffer_t *buffer, const pm_integer_t *integer) { /* An integer whose words are inline fits in a uint64_t, so it can be * appended directly. */ if (integer->length <= 2) { - const uint64_t value = ((uint64_t) integer->words.value[0]) | ((uint64_t) integer->words.value[1] << 32); - pm_buffer_append_format(buffer, "%" PRIu64, value); + pm_buffer_append_format(buffer, "%" PRIu64, pm_integer_uint64_value(integer)); return; } diff --git a/src/static_literals.c b/src/static_literals.c index edac044a8b..5579c3eea0 100644 --- a/src/static_literals.c +++ b/src/static_literals.c @@ -40,7 +40,7 @@ static uint64_t integer_hash(const pm_integer_t *integer) { uint64_t magnitude = (integer->length > 2) ? pm_xxh3((const uint8_t *) integer->words.values, sizeof(uint32_t) * integer->length) - : (((uint64_t) integer->words.value[1] << 32) | integer->words.value[0]); + : pm_integer_uint64_value(integer); return key_hash(PM_INTEGER_NODE, integer->negative, magnitude); } @@ -125,7 +125,7 @@ int64_value(const pm_parser_t *parser, const pm_node_t *node) { const pm_integer_t *integer = &((const pm_integer_node_t *) node)->value; if (integer->length > 1) return integer->negative ? INT64_MIN : INT64_MAX; - int64_t value = (int64_t) integer->words.value[0]; + int64_t value = (int64_t) pm_integer_uint64_value(integer); return integer->negative ? -value : value; } case PM_SOURCE_LINE_NODE: diff --git a/templates/ext/prism/api_node.c.erb b/templates/ext/prism/api_node.c.erb index 84e16c7e1b..a9d24d882d 100644 --- a/templates/ext/prism/api_node.c.erb +++ b/templates/ext/prism/api_node.c.erb @@ -54,7 +54,7 @@ pm_integer_new(const pm_integer_t *integer) { /* An integer whose words are inline fits in an unsigned long long, so it * converts directly unless it is negative and too large to negate. */ if (length <= 2) { - unsigned long long value = ((unsigned long long) integer->words.value[1] << 32) | integer->words.value[0]; + unsigned long long value = pm_integer_uint64_value(integer); if (!integer->negative) return ULL2NUM(value); if (value <= LLONG_MAX) return LL2NUM(-(long long) value); } diff --git a/test/prism/result/static_inspect_test.rb b/test/prism/result/static_inspect_test.rb index 51752177e4..3f5bcb92ad 100644 --- a/test/prism/result/static_inspect_test.rb +++ b/test/prism/result/static_inspect_test.rb @@ -47,6 +47,17 @@ def test_rational assert_equal "(1/1)", static_inspect("1r") assert_equal "(1/1)", static_inspect("1.0r") assert_equal "(77777/1000)", static_inspect("77.777r") + + assert_equal "(1/2)", static_inspect("0.5r") + assert_equal "(1/16)", static_inspect("0.0625r") + assert_equal "(21/2)", static_inspect("1_0.5_0r") + assert_equal "(1/1048576)", static_inspect("0.00000095367431640625r") + + assert_rational_inspect("0.#{(5 ** 60).to_s.rjust(60, "0")}r") + assert_rational_inspect("0.#{(3 * 2 ** 100).to_s.rjust(100, "0")}r") + assert_rational_inspect("#{7 ** 40}.0000025r") + assert_rational_inspect("1.#{"0" * 40}1r") + assert_rational_inspect("#{"9" * 300}.#{"5" * 300}r") end def test_regular_expression @@ -91,5 +102,9 @@ def static_inspect(source, **options) warnings = Prism.parse("{ #{source} => 1, #{source} => 1 }", **options).warnings warnings.last.message[/^key (.+) is duplicated and overwritten on line \d/, 1] end + + def assert_rational_inspect(source) + assert_equal eval(source).inspect, static_inspect(source) + end end end From c8e5dd9f9dde92aa24ebd6f17bbc56ab243fc7a1 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 2 Nov 2026 12:25:00 -0500 Subject: [PATCH 11/14] pm_arena_shrink -> pm_arena_realloc --- include/prism/internal/arena.h | 33 ++++++++++++++++++++++++--------- src/integer.c | 13 ++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/include/prism/internal/arena.h b/include/prism/internal/arena.h index 3327654382..d75b4a62f0 100644 --- a/include/prism/internal/arena.h +++ b/include/prism/internal/arena.h @@ -8,7 +8,6 @@ #include "prism/arena.h" -#include #include #include @@ -107,15 +106,31 @@ pm_arena_memdup(pm_arena_t *arena, const void *src, size_t size, size_t alignmen } /* - * Shrink the most recent allocation from the arena to new_size bytes, giving - * the rest back to the arena. The allocation must be the most recent one, so - * that it ends exactly where the current block's used bytes do. + * Resize an allocation from the arena from old_size to new_size bytes and + * return where it now lives. The sizes are in the same order as xrealloc_sized. + * The most recent allocation in the current block ends exactly where the + * block's used bytes do, so it is resized in place when the block has room. Any + * other allocation stays where it is when it shrinks, and is copied into a new + * allocation when it grows, leaving the old one behind in the arena. */ -static PRISM_INLINE void -pm_arena_shrink(pm_arena_t *arena, void *ptr, size_t size, size_t new_size) { - assert(new_size <= size); - assert(arena->current != NULL && (char *) ptr + size == arena->current->data + arena->current->used); - arena->current->used -= size - new_size; +static PRISM_INLINE void * +pm_arena_realloc(pm_arena_t *arena, void *ptr, size_t new_size, size_t old_size, size_t alignment) { + pm_arena_block_t *block = arena->current; + + if (block != NULL && (char *) ptr + old_size == block->data + block->used) { + size_t start = block->used - old_size; + + if (new_size <= block->capacity - start) { + block->used = start + new_size; + return ptr; + } + } + + if (new_size <= old_size) return ptr; + + void *result = pm_arena_alloc(arena, new_size, alignment); + memcpy(result, ptr, old_size); + return result; } #endif diff --git a/src/integer.c b/src/integer.c index bf0e646a04..bfd1851440 100644 --- a/src/integer.c +++ b/src/integer.c @@ -91,20 +91,19 @@ pm_integer_separator(uint8_t character) { } /** - * Store a bignum whose words are the most recent allocation in the arena, with - * room for capacity words, into an integer. An integer with at most two words - * stores them inline, so all of the arena words are given back; otherwise only - * the unused ones are. + * Store a bignum whose words are allocated in the arena, with room for capacity + * words, into an integer. An integer with at most two words stores them inline, + * so all of the arena words are given back; otherwise only the unused ones are. */ static void pm_integer_from_bignum(pm_arena_t *arena, pm_integer_t *integer, const pm_bignum_t *bignum, uint32_t capacity) { if (bignum->length <= 2) { uint64_t value = pm_bignum_uint64(bignum); - pm_arena_shrink(arena, bignum->words, sizeof(uint32_t) * capacity, 0); + pm_arena_realloc(arena, bignum->words, 0, sizeof(uint32_t) * capacity, PRISM_ALIGNOF(uint32_t)); *integer = pm_integer_uint64(value); } else { - pm_arena_shrink(arena, bignum->words, sizeof(uint32_t) * capacity, sizeof(uint32_t) * bignum->length); - *integer = (pm_integer_t) { .length = bignum->length, .words = { .values = bignum->words } }; + uint32_t *words = (uint32_t *) pm_arena_realloc(arena, bignum->words, sizeof(uint32_t) * bignum->length, sizeof(uint32_t) * capacity, PRISM_ALIGNOF(uint32_t)); + *integer = (pm_integer_t) { .length = bignum->length, .words = { .values = words } }; } } From fa38c9798260c102a1e7134406eacfeb8d62bb62 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 2 Nov 2026 17:18:41 -0500 Subject: [PATCH 12/14] Parse floats ourselves --- config.yml | 1 - include/prism/internal/double.h | 19 ++ prism.gemspec | 2 + src/double.c | 394 ++++++++++++++++++++++++++ src/prism.c | 74 +---- templates/src/diagnostic.c.erb | 1 - test/prism/result/float_parse_test.rb | 59 ++++ test/prism/result/warnings_test.rb | 7 + 8 files changed, 490 insertions(+), 67 deletions(-) create mode 100644 include/prism/internal/double.h create mode 100644 src/double.c create mode 100644 test/prism/result/float_parse_test.rb diff --git a/config.yml b/config.yml index 14ea82f405..f157187e8e 100644 --- a/config.yml +++ b/config.yml @@ -124,7 +124,6 @@ errors: - EXPRESSION_NOT_WRITABLE_NUMBERED - EXPRESSION_NOT_WRITABLE_SELF - EXPRESSION_NOT_WRITABLE_TRUE - - FLOAT_PARSE - FOR_COLLECTION - FOR_IN - FOR_INDEX diff --git a/include/prism/internal/double.h b/include/prism/internal/double.h new file mode 100644 index 0000000000..3c26c4397f --- /dev/null +++ b/include/prism/internal/double.h @@ -0,0 +1,19 @@ +/* + * This module provides a function for parsing the values of float literals. + */ +#ifndef PRISM_INTERNAL_DOUBLE_H +#define PRISM_INTERNAL_DOUBLE_H + +#include +#include + +/* + * Parse a decimal float literal into the nearest double, rounding ties to even. + * This assumes that the format of the literal has already been validated. It + * may start with a plus sign, which unary plus on a numeric literal leaves in + * its token. Underscores between digits are skipped. Sets out_of_range when the + * literal is nonzero but its value rounds to zero or to infinity. + */ +double pm_double_parse(const uint8_t *start, const uint8_t *end, bool *out_of_range); + +#endif diff --git a/prism.gemspec b/prism.gemspec index b1540b922e..4d8221f271 100644 --- a/prism.gemspec +++ b/prism.gemspec @@ -76,6 +76,7 @@ Gem::Specification.new do |spec| "include/prism/internal/comments.h", "include/prism/internal/constant_pool.h", "include/prism/internal/diagnostic.h", + "include/prism/internal/double.h", "include/prism/internal/encoding.h", "include/prism/internal/integer.h", "include/prism/internal/isinf.h", @@ -223,6 +224,7 @@ Gem::Specification.new do |spec| "src/comments.c", "src/constant_pool.c", "src/diagnostic.c", + "src/double.c", "src/encoding.c", "src/errors_format.c", "src/integer.c", diff --git a/src/double.c b/src/double.c new file mode 100644 index 0000000000..baa67edcea --- /dev/null +++ b/src/double.c @@ -0,0 +1,394 @@ +#include "prism/internal/double.h" + +#include "prism/internal/bignum.h" +#include "prism/internal/bit.h" + +#include +#include +#include +#include +#include + +/* The conversion builds the bits of the result directly, so it depends on the + * layout of a binary64 double. */ +#if FLT_RADIX != 2 || DBL_MANT_DIG != 53 || DBL_MIN_EXP != -1021 || DBL_MAX_EXP != 1024 +#error "prism requires IEEE 754 binary64 doubles" +#endif + +/** + * The number of leading significant digits that are converted exactly. + */ +#define PM_DOUBLE_DIGITS 768 + +/** + * The number of words in each side of the division in the exact conversion. + */ +#define PM_DOUBLE_WORDS 84 + +/** + * The explicit exponent stops growing once it reaches this. The literal's + * digits can only shift the exponent by its length, so past this the value is + * already infinity or zero, and capping it keeps the arithmetic far from + * overflowing an int64_t. + */ +#define PM_DOUBLE_EXPONENT_MAXIMUM 100000000000000000 + +/** + * The bits of positive infinity. + */ +#define PM_DOUBLE_INFINITY ((uint64_t) 0x7FF0000000000000) + +/** + * The largest power of five that fits in a word. + */ +#define PM_DOUBLE_POWER5_13 1220703125 + +/** + * The powers of ten that are exactly representable as doubles. + */ +static const double pm_double_powers[] = { + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, + 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22 +}; + +/** + * Return the number of bits in a nonzero number. + */ +static uint64_t +pm_double_bit_length(const pm_bignum_t *bignum) { + assert(bignum->length > 0); + return (uint64_t) (bignum->length - 1) * 32 + pm_bsrll(bignum->words[bignum->length - 1]) + 1; +} + +/** + * Multiply the number by five to the given power, in place. + */ +static void +pm_double_multiply_powof5(pm_bignum_t *bignum, uint64_t power) { + for (; power >= 13; power -= 13) pm_bignum_mul_add(bignum, PM_DOUBLE_POWER5_13, 0); + + uint32_t multiplier = 1; + for (; power > 0; power--) multiplier *= 5; + if (multiplier > 1) pm_bignum_mul_add(bignum, multiplier, 0); +} + +/** + * Divide the numerator by the divisor, where the quotient is known to fit in 64 + * bits. The divisor must have at least two words and its top bit set, and the + * numerator must have room for one more word above its length. + * + * Returns the quotient, sets inexact if the remainder is nonzero, and leaves + * the remainder in the numerator's low words, untrimmed. + */ +static uint64_t +pm_double_divide(pm_bignum_t *numerator, const pm_bignum_t *divisor, bool *inexact) { + uint32_t *numerator_words = numerator->words; + const uint32_t *divisor_words = divisor->words; + uint32_t numerator_length = numerator->length; + uint32_t divisor_length = divisor->length; + + assert(divisor_length >= 2 && numerator_length >= divisor_length && (divisor_words[divisor_length - 1] >> 31) == 1); + + uint64_t top = divisor_words[divisor_length - 1]; + uint64_t next = divisor_words[divisor_length - 2]; + uint64_t quotient = 0; + numerator_words[numerator_length] = 0; + + for (uint32_t index = numerator_length - divisor_length + 1; index > 0; index--) { + uint32_t *window = numerator_words + index - 1; + uint64_t current = ((uint64_t) window[divisor_length] << 32) | window[divisor_length - 1]; + uint64_t estimate = current / top; + uint64_t remainder = current % top; + + while (estimate > UINT32_MAX || estimate * next > ((remainder << 32) | window[divisor_length - 2])) { + estimate--; + remainder += top; + if (remainder > UINT32_MAX) break; + } + + /* A difference below zero wraps around, which sets its top bit. */ + uint64_t carry = 0; + uint64_t borrow = 0; + + for (uint32_t offset = 0; offset < divisor_length; offset++) { + uint64_t product = estimate * divisor_words[offset] + carry; + carry = product >> 32; + + uint64_t difference = (uint64_t) window[offset] - (uint32_t) product - borrow; + window[offset] = (uint32_t) difference; + borrow = difference >> 63; + } + + uint64_t difference = (uint64_t) window[divisor_length] - carry - borrow; + window[divisor_length] = (uint32_t) difference; + + if ((difference >> 63) != 0) { + estimate--; + carry = 0; + + for (uint32_t offset = 0; offset < divisor_length; offset++) { + uint64_t sum = (uint64_t) window[offset] + divisor_words[offset] + carry; + window[offset] = (uint32_t) sum; + carry = sum >> 32; + } + + window[divisor_length] += (uint32_t) carry; + } + + assert((quotient >> 32) == 0); + quotient = (quotient << 32) | estimate; + } + + for (uint32_t index = 0; index < divisor_length; index++) { + if (numerator_words[index] != 0) { + *inexact = true; + break; + } + } + + return quotient; +} + +/** + * Return the bits of the double nearest to (mantissa + fraction) * 2**exponent, + * rounding ties to even, where the fraction is in [0, 1) and inexact says + * whether it is nonzero. When inexact is set the mantissa must have more bits + * than a double keeps, so that the fraction only ever breaks ties. + */ +static uint64_t +pm_double_round(uint64_t mantissa, int64_t exponent, bool inexact) { + assert(mantissa != 0); + + /* Drop the bits below the 53 that a double keeps, or below the smallest + * subnormal if the value is that small. */ + int64_t shift = (int64_t) pm_bsrll(mantissa) + 1 - 53; + if (shift < -1074 - exponent) shift = -1074 - exponent; + + uint64_t bits; + if (shift <= 0) { + assert(!inexact); + bits = mantissa << -shift; + } else if (shift > 64) { + return 0; + } else { + uint64_t half = (uint64_t) 1 << (shift - 1); + uint64_t rest = mantissa & (half - 1 + half); + bits = shift == 64 ? 0 : mantissa >> shift; + if (rest > half || (rest == half && (inexact || (bits & 1)))) bits++; + } + + /* The bits hold the significand with its implicit bit, so adding the + * exponent field on top makes that bit carry into it. That also moves a + * subnormal that rounded up into the normal range, and a significand that + * rounded up to 2**53 into the next binade. */ + int64_t scale = exponent + shift; + if (scale > 971) return PM_DOUBLE_INFINITY; + + bits += (uint64_t) (scale + 1074) << 52; + return bits < PM_DOUBLE_INFINITY ? bits : PM_DOUBLE_INFINITY; +} + +/** + * Return the bits of the double nearest to the significant digits at start + * times 10**exponent. When there are at most 19 digits, value holds them. + */ +static uint64_t +pm_double_exact(const uint8_t *start, uint32_t digits, int64_t exponent, uint64_t value) { + uint32_t numerator_words[PM_DOUBLE_WORDS]; + pm_bignum_t numerator = { .length = 0, .words = numerator_words }; + + if (digits <= 19) { + numerator_words[0] = (uint32_t) value; + numerator_words[1] = (uint32_t) (value >> 32); + numerator.length = value > UINT32_MAX ? 2 : 1; + } else { + uint32_t remaining = digits > PM_DOUBLE_DIGITS ? PM_DOUBLE_DIGITS : digits; + uint32_t chunk = 0; + uint32_t multiplier = 1; + + for (const uint8_t *cursor = start; remaining > 0; cursor++) { + if (*cursor < '0' || *cursor > '9') continue; + + chunk = chunk * 10 + (uint32_t) (*cursor - '0'); + multiplier *= 10; + remaining--; + + if (multiplier == 1000000000) { + pm_bignum_mul_add(&numerator, multiplier, chunk); + chunk = 0; + multiplier = 1; + } + } + + if (digits > PM_DOUBLE_DIGITS) { + chunk = chunk * 10 + 1; + multiplier *= 10; + exponent += (int64_t) (digits - PM_DOUBLE_DIGITS) - 1; + } + + if (multiplier > 1) pm_bignum_mul_add(&numerator, multiplier, chunk); + } + + bool inexact = false; + uint64_t mantissa; + int64_t binary; + + /* Since 10**exp is 5**exp * 2**exp, only the power of five needs + * arithmetic; the power of 2 goes into the binary exponent. */ + if (exponent >= 0) { + /* Multiply by the power of five and round the top 64 bits. */ + pm_double_multiply_powof5(&numerator, (uint64_t) exponent); + + uint64_t length = pm_double_bit_length(&numerator); + binary = exponent; + + if (length > 64) { + uint64_t dropped = length - 64; + inexact = pm_bignum_trailing_zeros(&numerator) < dropped; + pm_bignum_shift_right(&numerator, dropped); + binary += (int64_t) dropped; + } + + mantissa = pm_bignum_uint64(&numerator); + } else { + /* Divide by the power of five and round the 64-bit quotient. */ + uint32_t denominator_words[PM_DOUBLE_WORDS]; + pm_bignum_t denominator = { .length = 1, .words = denominator_words }; + + denominator_words[0] = 1; + pm_double_multiply_powof5(&denominator, (uint64_t) -exponent); + + /* Shift the denominator so that its top bit is set and it has at least + * two words, and the numerator so that it has 63 more bits than the + * denominator. If the numerator is already longer than that, the + * denominator shifts by more whole words instead. */ + int64_t numerator_bits = (int64_t) pm_double_bit_length(&numerator); + int64_t denominator_bits = (int64_t) pm_double_bit_length(&denominator); + + int64_t denominator_shift = (32 - denominator_bits % 32) % 32; + if (denominator_bits + denominator_shift == 32) denominator_shift += 32; + + int64_t numerator_shift = 63 + denominator_bits + denominator_shift - numerator_bits; + if (numerator_shift < 0) { + denominator_shift += (31 - numerator_shift) / 32 * 32; + numerator_shift = 63 + denominator_bits + denominator_shift - numerator_bits; + } + + pm_bignum_shift_left(&numerator, (size_t) numerator_shift); + pm_bignum_shift_left(&denominator, (size_t) denominator_shift); + assert(numerator.length < PM_DOUBLE_WORDS && denominator.length <= PM_DOUBLE_WORDS); + + mantissa = pm_double_divide(&numerator, &denominator, &inexact); + binary = exponent + denominator_shift - numerator_shift; + } + + return pm_double_round(mantissa, binary, inexact); +} + +/** + * Parse a decimal float literal into the nearest double, rounding ties to even. + */ +double +pm_double_parse(const uint8_t *start, const uint8_t *end, bool *out_of_range) { + *out_of_range = false; + if (start < end && *start == '+') start++; + + const uint8_t *first = NULL; + const uint8_t *cursor = start; + + uint32_t digits = 0; + uint32_t zeros = 0; + + uint64_t value = 0; + int64_t exponent = 0; + + bool fraction = false; + bool e = false; + + for (; !e && cursor < end; cursor++) { + switch (*cursor) { + case '_': + break; + case '.': + fraction = true; + break; + case 'e': case 'E': + e = true; + break; + case '0': + if (fraction) exponent--; + if (first != NULL) zeros++; + break; + case '1': case '2': case '3': case '4': case '5': + case '6': case '7': case '8': case '9': + if (fraction) exponent--; + if (first == NULL) first = cursor; + + digits += zeros + 1; + if (digits <= 19) { + for (; zeros > 0; zeros--) value *= 10; + value = value * 10 + (uint64_t) (*cursor - '0'); + } else { + zeros = 0; + } + break; + default: + assert(false && "unreachable"); + break; + } + } + + exponent += (int64_t) zeros; + if (digits == 0) return 0.0; + + if (e) { + bool positive = true; + if (cursor < end && (*cursor == '+' || *cursor == '-')) { + positive = *cursor == '+'; + cursor++; + } + + int64_t power = 0; + for (; cursor < end; cursor++) { + if (*cursor != '_' && power < PM_DOUBLE_EXPONENT_MAXIMUM) power = power * 10 + (*cursor - '0'); + } + + exponent += positive ? power : -power; + } + + int64_t magnitude = (int64_t) digits + exponent; + uint64_t bits; + + if (magnitude > 309) { + bits = PM_DOUBLE_INFINITY; + } else if (magnitude < -323) { + bits = 0; + } else { + /* When the digits and the power of ten are both exactly representable, + * a single correctly rounded multiplication or division gives the + * result. When the power is too large but the digits have room, part + * of the power moves into the digits. This depends on the operation + * rounding once, to a double, rather than to a wider type first. */ +#if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD == 0 + if (digits <= 19 && value <= ((uint64_t) 1 << 53)) { + if (exponent >= -22 && exponent < 0) { + return (double) value / pm_double_powers[-exponent]; + } else if (exponent >= 0 && exponent <= 22) { + return (double) value * pm_double_powers[exponent]; + } else if (exponent > 22 && exponent <= 22 + 15) { + uint64_t scaled = value; + for (int64_t power = exponent; power > 22 && scaled <= ((uint64_t) 1 << 53); power--) scaled *= 10; + if (scaled <= ((uint64_t) 1 << 53)) return (double) scaled * 1e22; + } + } +#endif + + bits = pm_double_exact(first, digits, exponent, value); + } + + *out_of_range = bits == 0 || bits == PM_DOUBLE_INFINITY; + + double result; + memcpy(&result, &bits, sizeof(double)); + return result; +} diff --git a/src/prism.c b/src/prism.c index ff228368ed..6dc4ac0c8c 100644 --- a/src/prism.c +++ b/src/prism.c @@ -11,9 +11,9 @@ #include "prism/internal/comments.h" #include "prism/internal/constant_pool.h" #include "prism/internal/diagnostic.h" +#include "prism/internal/double.h" #include "prism/internal/encoding.h" #include "prism/internal/integer.h" -#include "prism/internal/isinf.h" #include "prism/internal/line_offset_list.h" #include "prism/internal/list.h" #include "prism/internal/magic_comments.h" @@ -37,10 +37,7 @@ #include "prism/version.h" #include -#include #include -#include -#include #include #include @@ -4097,65 +4094,16 @@ pm_find_pattern_node_create(pm_parser_t *parser, pm_node_list_t *nodes) { } /** - * Parse the value of a double, add appropriate errors if there is an issue, and - * return the value that should be saved on the PM_FLOAT_NODE node. + * Parse the value of a double, add a warning if it is out of range, and return + * the value that should be saved on the PM_FLOAT_NODE node. */ static double -pm_double_parse(pm_parser_t *parser, const pm_token_t *token) { - ptrdiff_t diff = token->end - token->start; - if (diff <= 0) return 0.0; +pm_float_node_value(pm_parser_t *parser, const pm_token_t *token) { + bool out_of_range; + double value = pm_double_parse(token->start, token->end, &out_of_range); - // First, get a buffer of the content. - size_t length = (size_t) diff; - const size_t buffer_size = sizeof(char) * (length + 1); - char *buffer = xmalloc(buffer_size); - if (buffer == NULL) PRISM_OOM(); - - memcpy((void *) buffer, token->start, length); - - // Next, determine if we need to replace the decimal point because of - // locale-specific options, and then normalize them if we have to. - char decimal_point = *localeconv()->decimal_point; - if (decimal_point != '.') { - for (size_t index = 0; index < length; index++) { - if (buffer[index] == '.') buffer[index] = decimal_point; - } - } - - /* - * Next, handle underscores by removing them from the buffer in a single - * pass to avoid quadratic-time copies. - */ - { - size_t write = 0; - for (size_t read = 0; read < length; read++) { - if (buffer[read] != '_') buffer[write++] = buffer[read]; - } - length = write; - } - - // Null-terminate the buffer so that strtod cannot read off the end. - buffer[length] = '\0'; - - // Now, call strtod to parse the value. Note that CRuby has their own - // version of strtod which avoids locales. We're okay using the locale-aware - // version because we've already validated through the parser that the token - // is in a valid format. - errno = 0; - char *eptr; - double value = strtod(buffer, &eptr); - - // This should never happen, because we've already checked that the token - // is in a valid format. However it's good to be safe. - if ((eptr != buffer + length) || (errno != 0 && errno != ERANGE)) { - PM_PARSER_ERR_TOKEN_FORMAT_CONTENT(parser, token, PM_ERR_FLOAT_PARSE); - xfree_sized(buffer, buffer_size); - return 0.0; - } - - // If errno is set, then it should only be ERANGE. At this point we need to - // check if it's infinity (it should be). - if (errno == ERANGE && PRISM_ISINF(value)) { + if (out_of_range) { + size_t length = PM_TOKEN_LENGTH(token); int warn_width; const char *ellipsis; @@ -4168,11 +4116,8 @@ pm_double_parse(pm_parser_t *parser, const pm_token_t *token) { } pm_diagnostic_list_append_format(&parser->metadata_arena, &parser->warning_list, PM_TOKEN_START(parser, token), PM_TOKEN_LENGTH(token), PM_WARN_FLOAT_OUT_OF_RANGE, warn_width, (const char *) token->start, ellipsis); - value = (value < 0.0) ? -HUGE_VAL : HUGE_VAL; } - // Finally we can free the buffer and return the value. - xfree_sized(buffer, buffer_size); return value; } @@ -4188,7 +4133,7 @@ pm_float_node_create(pm_parser_t *parser, const pm_token_t *token) { ++parser->node_id, PM_NODE_FLAG_STATIC_LITERAL, PM_LOCATION_INIT_TOKEN(parser, token), - pm_double_parse(parser, token) + pm_float_node_value(parser, token) ); } @@ -23520,7 +23465,6 @@ pm_parse_err_is_fatal(pm_diagnostic_id_t diag_id) { case PM_ERR_ESCAPE_INVALID_UNICODE_SHORT: case PM_ERR_EXPRESSION_NOT_WRITABLE: case PM_ERR_EXPRESSION_NOT_WRITABLE_SELF: - case PM_ERR_FLOAT_PARSE: case PM_ERR_GLOBAL_VARIABLE_BARE: case PM_ERR_HASH_KEY: case PM_ERR_HEREDOC_IDENTIFIER: diff --git a/templates/src/diagnostic.c.erb b/templates/src/diagnostic.c.erb index 7283411186..6a2e380823 100644 --- a/templates/src/diagnostic.c.erb +++ b/templates/src/diagnostic.c.erb @@ -223,7 +223,6 @@ static const pm_diagnostic_data_t diagnostic_messages[PM_DIAGNOSTIC_ID_MAX] = { [PM_ERR_EXPRESSION_NOT_WRITABLE_NUMBERED] = { "Can't assign to numbered parameter %.2s", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPRESSION_NOT_WRITABLE_SELF] = { "Can't change the value of self", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPRESSION_NOT_WRITABLE_TRUE] = { "Can't assign to true", PM_ERROR_LEVEL_SYNTAX }, - [PM_ERR_FLOAT_PARSE] = { "could not parse the float '%.*s'", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_FOR_COLLECTION] = { "expected a collection after the `in` in a `for` statement", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_FOR_INDEX] = { "expected an index after `for`", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_FOR_IN] = { "expected an `in` after the index in a `for` statement", PM_ERROR_LEVEL_SYNTAX }, diff --git a/test/prism/result/float_parse_test.rb b/test/prism/result/float_parse_test.rb new file mode 100644 index 0000000000..c46807488c --- /dev/null +++ b/test/prism/result/float_parse_test.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module Prism + class FloatParseTest < TestCase + def test_float_parse + assert_float_parse("3ff8000000000000", "1.5") + assert_float_parse("4000000000000000", "+2.0") + assert_float_parse("42a2309d03c48000", "1_000.000_1e1_0") + assert_float_parse("3fd3333333333334", "0.30000000000000004") + assert_float_parse("44b52d02c7e14af6", "1e23") + assert_float_parse("0000000000000000", "0.0e99999999999999999999") + end + + def test_float_parse_ties + # 2**53 + 1 is halfway between two doubles, so it rounds to the even one, + # and anything past it rounds up. + assert_float_parse("4340000000000000", "9007199254740993.0") + assert_float_parse("4340000000000001", "9007199254740993.0000000000000000000000001") + + # 1 + 2**-53 is halfway between 1 and the next double. A nonzero digit + # far past the first 768 significant digits still breaks the tie. + halfway = "1.00000000000000011102230246251565404236316680908203125" + assert_float_parse("3ff0000000000000", halfway) + assert_float_parse("3ff0000000000001", "#{halfway}#{"0" * 800}1") + + # Just below the halfway point between two doubles near 7e-77. + assert_float_parse( + "30205e1d07346324", + "7.06768076391090364561370091321831414708692372912499013908562533927508" \ + "1336861732843770134770676005047023865780514951295302778156921388487571" \ + "7771532077839355963550153097003194004494490890037373953536103954320424" \ + "23635721206665039062499999999999E-77" + ) + end + + def test_float_parse_limits + assert_float_parse("7fefffffffffffff", "1.7976931348623157e308") + assert_float_parse("7ff0000000000000", "1.7976931348623159e308") + assert_float_parse("000fffffffffffff", "2.2250738585072011e-308") + assert_float_parse("0000000000000001", "4.9e-324") + assert_float_parse("0000000000000001", "2.4703282292062328e-324") + assert_float_parse("0000000000000000", "2.4703282292062327e-324") + + # The halfway point between the largest subnormal and the smallest + # normal, which rounds up to the even one. + assert_float_parse("0010000000000000", "#{(2**53 - 1) * 5**1075}e-1075") + end + + private + + def assert_float_parse(expected, source) + node = Prism.parse_statement(source) + assert_kind_of FloatNode, node + assert_equal expected, [node.value].pack("G").unpack1("H*"), source + end + end +end diff --git a/test/prism/result/warnings_test.rb b/test/prism/result/warnings_test.rb index a4e1d68790..cd3c89db2b 100644 --- a/test/prism/result/warnings_test.rb +++ b/test/prism/result/warnings_test.rb @@ -166,6 +166,13 @@ def test_duplicated_pattern_keys_resolve_encoding def test_float_out_of_range assert_warning("_ = 1.0e100000", "out of range") + assert_warning("_ = 1.7976931348623159e308", "out of range") + assert_warning("_ = 1e-400", "out of range") + assert_warning("_ = 2.4703282292062327e-324", "out of range") + + refute_warning("_ = 1e-310") + refute_warning("_ = 2.4703282292062328e-324") + refute_warning("_ = 0e99999") end def test_indentation_mismatch From bf1965b62ffc222fd9c7141fdd05997fe3f46731 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 2 Nov 2026 21:48:40 -0500 Subject: [PATCH 13/14] Various cleanup for the numbers overhaul --- rust/ruby-prism-sys/src/bindings.rs | 4 ++-- src/bignum.c | 1 + src/double.c | 4 ++-- src/integer.c | 4 +++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/rust/ruby-prism-sys/src/bindings.rs b/rust/ruby-prism-sys/src/bindings.rs index 6a96278f65..8c74da020e 100644 --- a/rust/ruby-prism-sys/src/bindings.rs +++ b/rust/ruby-prism-sys/src/bindings.rs @@ -10491,8 +10491,8 @@ pub enum pm_comment_type_t { PM_COMMENT_INLINE = 0, PM_COMMENT_EMBDOC = 1, } -/** The words of the magnitude. Read them through pm_integer_words rather - than directly. +/** The words of the magnitude. Read them through pm_integer_words or + pm_integer_uint64_value rather than directly. */ #[repr(C)] #[derive(Copy, Clone)] diff --git a/src/bignum.c b/src/bignum.c index af3e1fcab6..65be6fab29 100644 --- a/src/bignum.c +++ b/src/bignum.c @@ -44,6 +44,7 @@ pm_bignum_base_words_add(uint64_t base, uint32_t *left, uint32_t left_length, co */ static PRISM_FORCE_INLINE void pm_bignum_base_words_subtract(uint64_t base, uint32_t *left, uint32_t left_length, const uint32_t *right, uint32_t right_length) { + (void) left_length; uint32_t borrow = 0; uint32_t index = 0; diff --git a/src/double.c b/src/double.c index baa67edcea..e359076a64 100644 --- a/src/double.c +++ b/src/double.c @@ -194,7 +194,7 @@ pm_double_round(uint64_t mantissa, int64_t exponent, bool inexact) { */ static uint64_t pm_double_exact(const uint8_t *start, uint32_t digits, int64_t exponent, uint64_t value) { - uint32_t numerator_words[PM_DOUBLE_WORDS]; + uint32_t numerator_words[PM_DOUBLE_WORDS] = { 0 }; pm_bignum_t numerator = { .length = 0, .words = numerator_words }; if (digits <= 19) { @@ -245,7 +245,7 @@ pm_double_exact(const uint8_t *start, uint32_t digits, int64_t exponent, uint64_ if (length > 64) { uint64_t dropped = length - 64; inexact = pm_bignum_trailing_zeros(&numerator) < dropped; - pm_bignum_shift_right(&numerator, dropped); + pm_bignum_shift_right(&numerator, (size_t) dropped); binary += (int64_t) dropped; } diff --git a/src/integer.c b/src/integer.c index bfd1851440..f07cdf0d04 100644 --- a/src/integer.c +++ b/src/integer.c @@ -119,6 +119,8 @@ pm_integer_parse_powof2(pm_arena_t *arena, pm_integer_t *integer, uint32_t base, uint32_t leading_bits = digit_bits[pm_integer_parse_digit(*start)]; uint32_t bit = (uint32_t) pm_ctzll(base); + assert(bit >= 1 && bit <= 4); + uint32_t length = (uint32_t) (((uint64_t) (digits - 1) * bit + leading_bits + 31) / 32); assert(length > 2); @@ -587,7 +589,7 @@ pm_integer_string_divide(pm_buffer_t *buffer, const uint32_t *words, uint32_t le for (uint32_t split = 2; split < length; split *= 2) { const pm_bignum_t *previous = &powers[level++]; - powers[level].words = pm_bignum_scratch_take(&scratch, 2 * previous->length); + powers[level] = (pm_bignum_t) { .length = 0, .words = pm_bignum_scratch_take(&scratch, 2 * previous->length) }; pm_bignum_decimal_multiply(&powers[level], previous, previous, &scratch); } From 235602951e2a28c5ec1e9976e2a3dd09795e2232 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 3 Nov 2026 09:39:41 -0500 Subject: [PATCH 14/14] Properly handle unary plus on rationals --- src/integer.c | 2 ++ test/prism/result/numeric_value_test.rb | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/integer.c b/src/integer.c index f07cdf0d04..7e4c209397 100644 --- a/src/integer.c +++ b/src/integer.c @@ -408,6 +408,8 @@ void pm_integer_parse_rational(pm_arena_t *arena, pm_integer_t *numerator, pm_integer_t *denominator, const uint8_t *start, const uint8_t *end) { static const uint32_t powof5[14] = { 1, 5, 25, 125, 625, 3125, 15625, 78125, 390625, 1953125, 9765625, 48828125, 244140625, 1220703125 }; + if (*start == '+') start++; + const uint8_t *point = memchr(start, '.', (size_t) (end - start)); assert(point != NULL); diff --git a/test/prism/result/numeric_value_test.rb b/test/prism/result/numeric_value_test.rb index 0207fa6a86..8ff57ab7de 100644 --- a/test/prism/result/numeric_value_test.rb +++ b/test/prism/result/numeric_value_test.rb @@ -28,5 +28,18 @@ def test_numeric_value assert_equal 0xFFri, Prism.parse_statement("0xFFri").value assert_equal 0xFFri, Prism.parse_statement("0xF_Fri").value end + + def test_unary_plus + assert_equal 123, Prism.parse_statement("+123").value + assert_equal 3.14, Prism.parse_statement("+3.14").value + assert_equal 42i, Prism.parse_statement("+42i").value + assert_equal 3.14i, Prism.parse_statement("+3.14i").value + assert_equal 42r, Prism.parse_statement("+42r").value + assert_equal 1.2r, Prism.parse_statement("+1.2r").value + assert_equal 1.2r, Prism.parse_statement("+1.2_0r").value + assert_equal 0r, Prism.parse_statement("+0.0r").value + assert_equal 42ri, Prism.parse_statement("+42ri").value + assert_equal 1.2ri, Prism.parse_statement("+1.2ri").value + end end end