mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat!: Use num-bigint for large numbers (#500)
- Supply chain: Use the popular num-bigint crate for handling large integers - Optimization: Handle f64, i64, u64 directly. These will be the most common instances of a number. OPA number semantics isn't clear. https://github.com/open-policy-agent/opa/issues/6281 As part of this change, we update the following failing tests: - A local test that relies on what 15.3/3 evaluates to. With our current change, we round in a different direction than what OPA does, but consistent with Rust. We produce 5.1000000000000005 where as the OPA test expects 5.1. There is no clear definition in Rego of what the right answer is. Moreover, policies should not rely on exact floating point value comparison. Therefore this deviations is justified. The test is patched to pass. - Another local vm test that exercised 1.1 + 2.2 - Another local vm test that exercises 5.5 - 2.2 - An OPA test that expects that a large integer number say 10e308 is printed in exponent notation. num-bigint does not print using scientific notation and instead prints all the digits. The benefit of preserving this compatibility is not clear. We skip this test. - Doc tests that exercised handling floating point numbers with more than 15 (what f64 supports) digits of precision. There is no usecase for this scenario. The tests are updated to reflect the behavior. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
14deaaa5b6
commit
a8a3a9809b
@@ -1,6 +1,9 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::ast::{Expr, Ref};
|
||||
use crate::builtins;
|
||||
use crate::builtins::utils::{ensure_args_count, ensure_string};
|
||||
@@ -88,25 +91,21 @@ fn parse(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Re
|
||||
_ => (string, ""),
|
||||
};
|
||||
|
||||
// Propagating the underlying error is not useful here.
|
||||
let v: Value = if number_part.starts_with('.') {
|
||||
serde_json::from_str(format!("0{number_part}").as_str())
|
||||
} else {
|
||||
serde_json::from_str(number_part)
|
||||
}
|
||||
.map_err(|_| params[0].span().error("could not parse number"))?;
|
||||
|
||||
let mut n = match v {
|
||||
Value::Number(n) => n.clone(),
|
||||
_ => bail!(span.error("could not parse number")),
|
||||
};
|
||||
// Canonicalize quirky literals (e.g. ".5", "-.1") so downstream parsing always sees
|
||||
// a stable representation that includes an explicit leading digit.
|
||||
let canonical_part = canonicalize_number_part(number_part);
|
||||
|
||||
if let Some(e) = ten_exp(suffix) {
|
||||
n.mul_assign(&Number::ten_pow(e)?)?;
|
||||
Ok(Value::from(n))
|
||||
let combined = combine_decimal_exponent(canonical_part.as_ref(), e)
|
||||
.ok_or_else(|| params[0].span().error("could not parse number"))?;
|
||||
let number = Number::from_str(&combined)
|
||||
.map_err(|_| params[0].span().error("could not parse number"))?;
|
||||
Ok(Value::from(number))
|
||||
} else if let Some(e) = two_exp(suffix) {
|
||||
n.mul_assign(&Number::two_pow(e)?)?;
|
||||
Ok(Value::from(n))
|
||||
let mut number = Number::from_str(canonical_part.as_ref())
|
||||
.map_err(|_| params[0].span().error("could not parse number"))?;
|
||||
number.mul_assign(&Number::two_pow(e)?)?;
|
||||
Ok(Value::from(number))
|
||||
} else {
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
@@ -166,28 +165,83 @@ fn parse_bytes(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool)
|
||||
_ => (string, ""),
|
||||
};
|
||||
|
||||
let v: Value = match if number_part.starts_with('.') {
|
||||
serde_json::from_str(format!("0{number_part}").as_str())
|
||||
} else {
|
||||
serde_json::from_str(number_part)
|
||||
} {
|
||||
Ok(v) => v,
|
||||
Err(_) if strict => bail!(span.error("could not parse number")),
|
||||
_ => return Ok(Value::Undefined),
|
||||
};
|
||||
|
||||
let mut n = match v {
|
||||
Value::Number(n) => n.clone(),
|
||||
_ => bail!(span.error("could not parse number")),
|
||||
};
|
||||
// Canonicalize quirky literals (e.g. ".5", "-.1") so downstream parsing always sees
|
||||
// a stable representation that includes an explicit leading digit.
|
||||
let canonical_part = canonicalize_number_part(number_part);
|
||||
|
||||
if let Some(e) = twob_exp(suffix) {
|
||||
n.mul_assign(&Number::two_pow(e)?)?;
|
||||
Ok(Value::from(n.round()))
|
||||
let mut number = match Number::from_str(canonical_part.as_ref()) {
|
||||
Ok(n) => n,
|
||||
Err(_) if strict => bail!(span.error("could not parse number")),
|
||||
Err(_) => return Ok(Value::Undefined),
|
||||
};
|
||||
number.mul_assign(&Number::two_pow(e)?)?;
|
||||
Ok(Value::from(number.round()))
|
||||
} else if let Some(e) = tenb_exp(suffix) {
|
||||
n.mul_assign(&Number::ten_pow(e)?)?;
|
||||
Ok(Value::from(n.round()))
|
||||
let combined = match combine_decimal_exponent(canonical_part.as_ref(), e) {
|
||||
Some(s) => s,
|
||||
None if strict => bail!(span.error("could not parse number")),
|
||||
None => return Ok(Value::Undefined),
|
||||
};
|
||||
let number = match Number::from_str(&combined) {
|
||||
Ok(n) => n,
|
||||
Err(_) if strict => bail!(span.error("could not parse number")),
|
||||
Err(_) => return Ok(Value::Undefined),
|
||||
};
|
||||
Ok(Value::from(number.round()))
|
||||
} else {
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
}
|
||||
|
||||
// Normalizes numbers that start with a decimal point by injecting the implicit leading zero.
|
||||
// This means inputs like ".5" and "-.1" become "0.5" / "-0.1", which Number::from_str accepts.
|
||||
fn canonicalize_number_part(number_part: &str) -> Cow<'_, str> {
|
||||
if let Some(rest) = number_part.strip_prefix("-.") {
|
||||
Cow::Owned(format!("-0.{rest}"))
|
||||
} else if let Some(rest) = number_part.strip_prefix("+.") {
|
||||
Cow::Owned(format!("+0.{rest}"))
|
||||
} else if let Some(rest) = number_part.strip_prefix('.') {
|
||||
Cow::Owned(format!("0.{rest}"))
|
||||
} else {
|
||||
Cow::Borrowed(number_part)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the unit-derived exponent (from suffix) to any exponent already present in the literal.
|
||||
// Produces a canonical `mantissa eX` string or returns None when the combined exponent overflows.
|
||||
fn combine_decimal_exponent(number: &str, suffix_exp: i32) -> Option<String> {
|
||||
if suffix_exp == 0 {
|
||||
return Some(number.to_string());
|
||||
}
|
||||
|
||||
let (mantissa, base_exp) = match split_decimal_exponent(number) {
|
||||
Some(parts) => parts,
|
||||
None => (number, 0),
|
||||
};
|
||||
|
||||
if mantissa.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let combined_exp = base_exp.checked_add(suffix_exp)?;
|
||||
|
||||
if combined_exp == 0 {
|
||||
Some(mantissa.to_string())
|
||||
} else {
|
||||
Some(format!("{}e{}", mantissa, combined_exp))
|
||||
}
|
||||
}
|
||||
|
||||
// Splits scientific notation ("1.2e3") into mantissa/exponent so callers can adjust the exponent.
|
||||
// Returns None when no exponent exists or the tail cannot be parsed into a 32-bit integer.
|
||||
fn split_decimal_exponent(number: &str) -> Option<(&str, i32)> {
|
||||
let idx = number.find(['e', 'E'])?;
|
||||
let mantissa = &number[..idx];
|
||||
let exp_part = &number[idx + 1..];
|
||||
if exp_part.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let exponent = exp_part.parse::<i32>().ok()?;
|
||||
Some((mantissa, exponent))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user