mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
- 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>
248 lines
7.6 KiB
Rust
248 lines
7.6 KiB
Rust
// 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};
|
|
use crate::lexer::Span;
|
|
use crate::number::Number;
|
|
use crate::value::Value;
|
|
use crate::*;
|
|
|
|
use anyhow::{bail, Result};
|
|
|
|
pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
|
|
m.insert("units.parse", (parse, 1));
|
|
m.insert("units.parse_bytes", (parse_bytes, 1));
|
|
}
|
|
|
|
fn ten_exp(suffix: &str) -> Option<i32> {
|
|
Some(match suffix {
|
|
"E" | "e" => 18,
|
|
"P" | "p" => 15,
|
|
"T" | "t" => 12,
|
|
"G" | "g" => 9,
|
|
"M" => 6,
|
|
"K" | "k" => 3,
|
|
"m" => -3,
|
|
|
|
// The following are not supported by OPA
|
|
"Q" => 30,
|
|
"R" => 27,
|
|
"Y" => 24,
|
|
"Z" => 21,
|
|
"h" => 2,
|
|
"da" => 1,
|
|
"d" => -1,
|
|
"c" => -2,
|
|
|
|
"μ" => -6,
|
|
"n" => -9,
|
|
"f" => -15,
|
|
"a" => -18,
|
|
"z" => -21,
|
|
"y" => -24,
|
|
"r" => -27,
|
|
"q" => -30,
|
|
|
|
// No suffix specified.
|
|
"" => 0,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
fn two_exp(suffix: &str) -> Option<i32> {
|
|
Some(match suffix.to_ascii_lowercase().as_str() {
|
|
"ki" => 10,
|
|
"mi" => 20,
|
|
"gi" => 30,
|
|
"ti" => 40,
|
|
"pi" => 50,
|
|
"ei" => 60,
|
|
"zi" => 70,
|
|
"yi" => 80,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
fn parse(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
|
let name = "units.parse";
|
|
ensure_args_count(span, name, params, args, 1)?;
|
|
let string = ensure_string(name, ¶ms[0], &args[0])?;
|
|
let string = string.as_ref();
|
|
|
|
// Remove quotes.
|
|
let string = if string.starts_with('"') && string.ends_with('"') && string.len() >= 2 {
|
|
&string[1..string.len() - 1]
|
|
} else {
|
|
string
|
|
};
|
|
|
|
// Disallow whitespace.
|
|
if string.chars().any(char::is_whitespace) {
|
|
bail!(span.error("spaces not allowed in resource strings"));
|
|
}
|
|
|
|
let (number_part, suffix) = match string.rfind(|c: char| c.is_ascii_digit()) {
|
|
Some(p) => (&string[0..p + 1], &string[p + 1..]),
|
|
_ => (string, ""),
|
|
};
|
|
|
|
// 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) {
|
|
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) {
|
|
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)
|
|
}
|
|
}
|
|
|
|
fn twob_exp(suffix: &str) -> Option<i32> {
|
|
Some(match suffix.to_ascii_lowercase().as_str() {
|
|
"yi" | "yib" => 80,
|
|
"zi" | "zib" => 70,
|
|
"ei" | "eib" => 60,
|
|
"pi" | "pib" => 50,
|
|
"ti" | "tib" => 40,
|
|
"gi" | "gib" => 30,
|
|
"mi" | "mib" => 20,
|
|
"ki" | "kib" => 10,
|
|
"" => 0,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
fn tenb_exp(suffix: &str) -> Option<i32> {
|
|
Some(match suffix.to_ascii_lowercase().as_str() {
|
|
"q" | "qb" => 30,
|
|
"r" | "rb" => 27,
|
|
"y" | "yb" => 24,
|
|
"z" | "zb" => 21,
|
|
"e" | "eb" => 18,
|
|
"p" | "pb" => 15,
|
|
"t" | "tb" => 12,
|
|
"g" | "gb" => 9,
|
|
"m" | "mb" => 6,
|
|
"k" | "kb" => 3,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
fn parse_bytes(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
|
|
let name = "units.parse_bytes";
|
|
ensure_args_count(span, name, params, args, 1)?;
|
|
let string = ensure_string(name, ¶ms[0], &args[0])?;
|
|
let string = string.as_ref();
|
|
|
|
// Remove quotes.
|
|
let string = if string.starts_with('"') && string.ends_with('"') && string.len() >= 2 {
|
|
&string[1..string.len() - 1]
|
|
} else {
|
|
string
|
|
};
|
|
|
|
// Disallow whitespace.
|
|
if string.chars().any(char::is_whitespace) {
|
|
bail!(span.error("spaces not allowed in resource strings"));
|
|
}
|
|
|
|
let (number_part, suffix) = match string.rfind(|c: char| c.is_ascii_digit()) {
|
|
Some(p) => (&string[0..p + 1], &string[p + 1..]),
|
|
_ => (string, ""),
|
|
};
|
|
|
|
// 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) {
|
|
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) {
|
|
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))
|
|
}
|