difftreelog
feat return NumValue directly from parser
in: master
15 files changed
bindings/jsonnet/src/val_make.rsdiffbeforeafterboth--- a/bindings/jsonnet/src/val_make.rs
+++ b/bindings/jsonnet/src/val_make.rs
@@ -5,10 +5,7 @@
os::raw::{c_char, c_double, c_int},
};
-use jrsonnet_evaluator::{
- ObjValue, Val,
- val::{ArrValue, NumValue},
-};
+use jrsonnet_evaluator::{NumValue, ObjValue, Val};
use crate::VM;
crates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -2,7 +2,9 @@
use jrsonnet_gcmodule::{Acyclic, Trace};
use jrsonnet_interner::IStr;
-use jrsonnet_ir::{BinaryOpType, Source, SourcePath, Span, Spanned, UnaryOpType};
+use jrsonnet_ir::{
+ BinaryOpType, ConvertNumValueError, Source, SourcePath, Span, Spanned, UnaryOpType,
+};
use jrsonnet_types::ValType;
use thiserror::Error;
@@ -11,7 +13,6 @@
function::{CallLocation, FunctionSignature, ParamName},
stdlib::format::FormatError,
typed::TypeLocError,
- val::ConvertNumValueError,
};
#[derive(Debug, Clone)]
@@ -228,6 +229,11 @@
Self::new(e)
}
}
+impl From<ConvertNumValueError> for Error {
+ fn from(e: ConvertNumValueError) -> Self {
+ Self::new(ErrorKind::ConvertNumValue(e))
+ }
+}
impl From<Infallible> for Error {
fn from(_value: Infallible) -> Self {
crates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -21,7 +21,7 @@
function::{CallLocation, FuncDesc, FuncVal, PreparedFuncVal},
in_frame,
typed::{FromUntyped, IntoUntyped as _, Typed},
- val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},
+ val::{CachedUnbound, IndexableVal, StrValue, Thunk},
with_state,
};
pub mod destructure;
@@ -58,9 +58,7 @@
}
Some(match expr {
Expr::Str(s) => Val::string(s.clone()),
- Expr::Num(n) => {
- Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))
- }
+ Expr::Num(n) => Val::Num(*n),
Expr::Literal(LiteralType::False) => Val::Bool(false),
Expr::Literal(LiteralType::True) => Val::Bool(true),
Expr::Literal(LiteralType::Null) => Val::Null,
crates/jrsonnet-evaluator/src/integrations/serde.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/integrations/serde.rs
+++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs
@@ -1,6 +1,7 @@
use std::borrow::Cow;
use jrsonnet_interner::{IBytes, IStr};
+use jrsonnet_ir::NumValue;
use serde::{
Deserialize, Serialize, Serializer,
de::{self, Visitor},
@@ -12,7 +13,6 @@
use crate::{
Error as JrError, ObjValue, ObjValueBuilder, Result, Val, in_description_frame, runtime_error,
- val::NumValue,
};
impl<'de> Deserialize<'de> for Val {
crates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -42,6 +42,7 @@
use jrsonnet_gcmodule::{Cc, Trace, cc_dyn};
pub use jrsonnet_interner::{IBytes, IStr};
pub use jrsonnet_ir as parser;
+pub use jrsonnet_ir::NumValue;
use jrsonnet_ir::{Expr, Source, SourcePath};
#[doc(hidden)]
pub use jrsonnet_macros;
crates/jrsonnet-evaluator/src/stdlib/format.rsdiffbeforeafterboth1//! faster std.format impl2#![allow(clippy::too_many_arguments)]3#![expect(4 clippy::cast_possible_truncation,5 clippy::cast_sign_loss,6 reason = "many safe integer casts, behavior on overflow is not specified"7)]89use jrsonnet_gcmodule::Trace;10use jrsonnet_interner::IStr;11use jrsonnet_types::ValType;12use thiserror::Error;1314use crate::{15 Error, ObjValue, Result, Val, bail,16 error::{ErrorKind::*, format_found, suggest_object_fields},17 typed::FromUntyped,18};1920#[derive(Debug, Clone, Error, Trace)]21pub enum FormatError {22 #[error("truncated format code")]23 TruncatedFormatCode,24 #[error("unrecognized conversion type: {0}")]25 UnrecognizedConversionType(char),2627 #[error("not enough values")]28 NotEnoughValues,2930 #[error("cannot use * width with object")]31 CannotUseStarWidthWithObject,32 #[error("mapping keys required")]33 MappingKeysRequired,34 #[error("no such format field: {0}")]35 NoSuchFormatField(IStr),3637 #[error("expected subfield <{0}> to be an object, got {1} instead")]38 SubfieldDidntYieldAnObject(IStr, ValType),39 #[error("subfield not found: <[{full}]{current}>{}", format_found(.found, "subfield"))]40 SubfieldNotFound {41 current: IStr,42 full: IStr,43 found: Box<Vec<IStr>>,44 },45}4647impl From<FormatError> for Error {48 fn from(e: FormatError) -> Self {49 Self::new(Format(e))50 }51}5253use FormatError::*;5455type ParseResult<'t, T> = std::result::Result<(T, &'t str), FormatError>;5657pub fn try_parse_mapping_key(str: &str) -> ParseResult<'_, &str> {58 if str.is_empty() {59 return Err(TruncatedFormatCode);60 }61 let bytes = str.as_bytes();62 if bytes[0] == b'(' {63 let mut i = 1;64 while i < bytes.len() {65 if bytes[i] == b')' {66 return Ok((&str[1..i], &str[i + 1..]));67 }68 i += 1;69 }70 Err(TruncatedFormatCode)71 } else {72 Ok(("", str))73 }74}7576#[cfg(test)]77pub mod tests_key {78 use super::*;7980 #[test]81 fn parse_key() {82 assert_eq!(83 try_parse_mapping_key("(hello ) world").unwrap(),84 ("hello ", " world")85 );86 assert_eq!(try_parse_mapping_key("() world").unwrap(), ("", " world"));87 assert_eq!(try_parse_mapping_key(" world").unwrap(), ("", " world"));88 assert_eq!(89 try_parse_mapping_key(" () world").unwrap(),90 ("", " () world")91 );92 }9394 #[test]95 #[should_panic = "TruncatedFormatCode"]96 fn parse_key_missing_start() {97 try_parse_mapping_key("").unwrap();98 }99100 #[test]101 #[should_panic = "TruncatedFormatCode"]102 fn parse_key_missing_end() {103 try_parse_mapping_key("( ").unwrap();104 }105}106107#[allow(clippy::struct_excessive_bools)]108#[derive(Default, Debug)]109pub struct CFlags {110 pub alt: bool,111 pub zero: bool,112 pub left: bool,113 pub blank: bool,114 pub sign: bool,115}116117pub fn try_parse_cflags(str: &str) -> ParseResult<'_, CFlags> {118 if str.is_empty() {119 return Err(TruncatedFormatCode);120 }121 let bytes = str.as_bytes();122 let mut i = 0;123 let mut out = CFlags::default();124 loop {125 if bytes.len() == i {126 return Err(TruncatedFormatCode);127 }128 match bytes[i] {129 b'#' => out.alt = true,130 b'0' => out.zero = true,131 b'-' => out.left = true,132 b' ' => out.blank = true,133 b'+' => out.sign = true,134 _ => break,135 }136 i += 1;137 }138 Ok((out, &str[i..]))139}140141#[derive(Debug, PartialEq, Eq)]142pub enum Width {143 Star,144 Fixed(u16),145}146pub fn try_parse_field_width(str: &str) -> ParseResult<'_, Width> {147 if str.is_empty() {148 return Err(TruncatedFormatCode);149 }150 let bytes = str.as_bytes();151 if bytes[0] == b'*' {152 return Ok((Width::Star, &str[1..]));153 }154 let mut out: u16 = 0;155 let mut digits = 0;156 while let Some(digit) = (bytes[digits] as char).to_digit(10) {157 out *= 10;158 out += digit as u16;159 digits += 1;160 if digits == bytes.len() {161 return Err(TruncatedFormatCode);162 }163 }164 Ok((Width::Fixed(out), &str[digits..]))165}166167pub fn try_parse_precision(str: &str) -> ParseResult<'_, Option<Width>> {168 if str.is_empty() {169 return Err(TruncatedFormatCode);170 }171 let bytes = str.as_bytes();172 if bytes[0] == b'.' {173 try_parse_field_width(&str[1..]).map(|(r, s)| (Some(r), s))174 } else {175 Ok((None, str))176 }177}178179// Only skips180pub fn try_parse_length_modifier(str: &str) -> ParseResult<'_, ()> {181 if str.is_empty() {182 return Err(TruncatedFormatCode);183 }184 let bytes = str.as_bytes();185 let mut idx = 0;186 while bytes[idx] == b'h' || bytes[idx] == b'l' || bytes[idx] == b'L' {187 idx += 1;188 if bytes.len() == idx {189 return Err(TruncatedFormatCode);190 }191 }192 Ok(((), &str[idx..]))193}194195#[derive(Debug, PartialEq, Eq)]196pub enum ConvTypeV {197 Decimal,198 Octal,199 Hexadecimal,200 Scientific,201 Float,202 Shorter,203 Char,204 String,205 Percent,206}207pub struct ConvType {208 v: ConvTypeV,209 caps: bool,210}211212pub fn parse_conversion_type(str: &str) -> ParseResult<'_, ConvType> {213 if str.is_empty() {214 return Err(TruncatedFormatCode);215 }216217 let code = str.as_bytes()[0];218 let v: (ConvTypeV, bool) = match code {219 b'd' | b'i' | b'u' => (ConvTypeV::Decimal, false),220 b'o' => (ConvTypeV::Octal, false),221 b'x' => (ConvTypeV::Hexadecimal, false),222 b'X' => (ConvTypeV::Hexadecimal, true),223 b'e' => (ConvTypeV::Scientific, false),224 b'E' => (ConvTypeV::Scientific, true),225 b'f' => (ConvTypeV::Float, false),226 b'F' => (ConvTypeV::Float, true),227 b'g' => (ConvTypeV::Shorter, false),228 b'G' => (ConvTypeV::Shorter, true),229 b'c' => (ConvTypeV::Char, false),230 b's' => (ConvTypeV::String, false),231 b'%' => (ConvTypeV::Percent, false),232 c => return Err(UnrecognizedConversionType(c as char)),233 };234235 Ok((ConvType { v: v.0, caps: v.1 }, &str[1..]))236}237238#[derive(Debug)]239pub struct Code<'s> {240 mkey: &'s str,241 cflags: CFlags,242 width: Width,243 precision: Option<Width>,244 convtype: ConvTypeV,245 caps: bool,246}247pub fn parse_code(str: &str) -> ParseResult<'_, Code<'_>> {248 if str.is_empty() {249 return Err(TruncatedFormatCode);250 }251 let (mkey, str) = try_parse_mapping_key(str)?;252 let (cflags, str) = try_parse_cflags(str)?;253 let (width, str) = try_parse_field_width(str)?;254 let (precision, str) = try_parse_precision(str)?;255 let ((), str) = try_parse_length_modifier(str)?;256 let (convtype, str) = parse_conversion_type(str)?;257258 Ok((259 Code {260 mkey,261 cflags,262 width,263 precision,264 convtype: convtype.v,265 caps: convtype.caps,266 },267 str,268 ))269}270271#[derive(Debug)]272pub enum Element<'s> {273 String(&'s str),274 Code(Code<'s>),275}276pub fn parse_codes(mut str: &str) -> Result<Vec<Element<'_>>> {277 let mut bytes = str.as_bytes();278 let mut out = vec![];279 let mut offset = 0;280281 loop {282 while offset != bytes.len() && bytes[offset] != b'%' {283 offset += 1;284 }285 if offset != 0 {286 out.push(Element::String(&str[0..offset]));287 }288 if offset == bytes.len() {289 return Ok(out);290 }291 str = &str[offset + 1..];292 let code;293 (code, str) = parse_code(str)?;294 bytes = str.as_bytes();295 offset = 0;296297 out.push(Element::Code(code));298 }299}300301const NUMBERS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";302303#[inline]304#[allow(clippy::fn_params_excessive_bools)]305pub fn render_integer(306 out: &mut String,307 neg: bool,308 iv: f64,309 padding: u16,310 precision: u16,311 blank: bool,312 sign: bool,313 radix: i64,314 zero_prefix: &str,315 prefix_in_padding: bool,316 caps: bool,317) {318 debug_assert!(iv >= 0.0, "render_integer receives sign using arg");319 let iv = iv.floor() as i64;320 // Digit char indexes in reverse order, i.e321 // for radix = 16 and n = 12f: [15, 2, 1]322 let digits = if iv == 0 {323 vec![0u8]324 } else {325 let mut v = iv.abs();326 let mut nums = Vec::with_capacity(1);327 while v != 0 {328 nums.push((v % radix) as u8);329 v /= radix;330 }331 nums332 };333 #[allow(clippy::bool_to_int_with_if)]334 let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 });335336 let pref_len = zero_prefix.len() as u16;337 let zp2 = zp338 .saturating_sub(if prefix_in_padding { 0 } else { pref_len })339 .max(precision)340 .saturating_sub(if prefix_in_padding { pref_len } else { 0 } + digits.len() as u16);341342 if neg {343 out.push('-');344 } else if sign {345 out.push('+');346 } else if blank {347 out.push(' ');348 }349350 out.reserve(zp2 as usize);351 if iv != 0 {352 out.push_str(zero_prefix);353 }354 for _ in 0..zp2 {355 out.push('0');356 }357358 for digit in digits.into_iter().rev() {359 let ch = NUMBERS[digit as usize] as char;360 out.push(if caps { ch.to_ascii_uppercase() } else { ch });361 }362}363364pub fn render_decimal(365 out: &mut String,366 neg: bool,367 iv: f64,368 padding: u16,369 precision: u16,370 blank: bool,371 sign: bool,372) {373 render_integer(374 out, neg, iv, padding, precision, blank, sign, 10, "", false, false,375 );376}377#[allow(clippy::fn_params_excessive_bools)]378pub fn render_octal(379 out: &mut String,380 neg: bool,381 iv: f64,382 padding: u16,383 precision: u16,384 alt: bool,385 blank: bool,386 sign: bool,387) {388 render_integer(389 out,390 neg,391 iv,392 padding,393 precision,394 blank,395 sign,396 8,397 if alt && iv != 0.0 { "0" } else { "" },398 true,399 false,400 );401}402403#[allow(clippy::fn_params_excessive_bools)]404pub fn render_hexadecimal(405 out: &mut String,406 iv: f64,407 padding: u16,408 precision: u16,409 alt: bool,410 blank: bool,411 sign: bool,412 caps: bool,413) {414 render_integer(415 out,416 iv < 0.0,417 iv.abs(),418 padding,419 precision,420 blank,421 sign,422 16,423 match (alt, caps) {424 (true, true) => "0X",425 (true, false) => "0x",426 (false, _) => "",427 },428 false,429 caps,430 );431}432433#[allow(clippy::fn_params_excessive_bools)]434pub fn render_float(435 out: &mut String,436 n: f64,437 mut padding: u16,438 precision: u16,439 blank: bool,440 sign: bool,441 ensure_pt: bool,442 trailing: bool,443) {444 // Represent the rounded number as an integer * 1/10**prec.445 // Note that it can also be equal to 10**prec and we'll need to carry446 // over to the wholes. We operate on the absolute numbers, so that we447 // don't have trouble with the rounding direction.448 let denominator = 10.0f64.powi(i32::from(precision));449 let numerator = n.abs().mul_add(denominator, 0.5);450 let whole = (numerator / denominator).floor();451 let frac = numerator.floor() % denominator;452453 #[allow(clippy::bool_to_int_with_if)]454 let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 };455 padding = padding.saturating_sub(dot_size + precision);456 render_decimal(out, n < 0.0, whole, padding, 0, blank, sign);457 if precision == 0 {458 if ensure_pt {459 out.push('.');460 }461 return;462 }463 if trailing || frac > 0.0 {464 out.push('.');465 let mut frac_str = String::new();466 render_decimal(&mut frac_str, false, frac, precision, 0, false, false);467 let mut trim = frac_str.len();468 if !trailing {469 for b in frac_str.as_bytes().iter().rev() {470 if *b == b'0' {471 trim -= 1;472 } else {473 break;474 }475 }476 }477 out.push_str(&frac_str[..trim]);478 } else if ensure_pt {479 out.push('.');480 }481}482483#[allow(clippy::fn_params_excessive_bools)]484pub fn render_float_sci(485 out: &mut String,486 n: f64,487 mut padding: u16,488 precision: u16,489 blank: bool,490 sign: bool,491 ensure_pt: bool,492 trailing: bool,493 caps: bool,494) {495 let exponent = if n == 0.0 {496 0.0497 } else {498 n.abs().log10().floor()499 };500501 let mantissa = if exponent as i16 == -324 {502 n * 10.0 / 10.0_f64.powf(exponent + 1.0)503 } else {504 n / 10.0_f64.powf(exponent)505 };506 let mut exponent_str = String::new();507 render_decimal(508 &mut exponent_str,509 exponent < 0.0,510 exponent.abs(),511 3,512 0,513 false,514 true,515 );516517 // +1 for e518 padding = padding.saturating_sub(exponent_str.len() as u16 + 1);519520 render_float(521 out, mantissa, padding, precision, blank, sign, ensure_pt, trailing,522 );523 out.push(if caps { 'E' } else { 'e' });524 out.push_str(&exponent_str);525}526527#[allow(clippy::too_many_lines)]528pub fn format_code(529 out: &mut String,530 value: &Val,531 code: &Code<'_>,532 width: u16,533 precision: Option<u16>,534) -> Result<()> {535 let clfags = &code.cflags;536 let (fpprec, iprec) = precision.map_or((6, 0), |v| (v, v));537 let padding = if clfags.zero && !clfags.left {538 width539 } else {540 0541 };542543 // TODO: If left padded, can optimize by writing directly to out544 let mut tmp_out = String::new();545546 match code.convtype {547 ConvTypeV::String => tmp_out.push_str(&value.clone().to_string()?),548 ConvTypeV::Decimal => {549 let value = f64::from_untyped(value.clone())?;550 render_decimal(551 &mut tmp_out,552 value <= -1.0,553 value.abs(),554 padding,555 iprec,556 clfags.blank,557 clfags.sign,558 );559 }560 ConvTypeV::Octal => {561 let value = f64::from_untyped(value.clone())?;562 render_octal(563 &mut tmp_out,564 value <= -1.0,565 value.abs(),566 padding,567 iprec,568 clfags.alt,569 clfags.blank,570 clfags.sign,571 );572 }573 ConvTypeV::Hexadecimal => {574 let value = f64::from_untyped(value.clone())?;575 render_hexadecimal(576 &mut tmp_out,577 value,578 padding,579 iprec,580 clfags.alt,581 clfags.blank,582 clfags.sign,583 code.caps,584 );585 }586 ConvTypeV::Scientific => {587 let value = f64::from_untyped(value.clone())?;588 render_float_sci(589 &mut tmp_out,590 value,591 padding,592 fpprec,593 clfags.blank,594 clfags.sign,595 clfags.alt,596 true,597 code.caps,598 );599 }600 ConvTypeV::Float => {601 let value = f64::from_untyped(value.clone())?;602 render_float(603 &mut tmp_out,604 value,605 padding,606 fpprec,607 clfags.blank,608 clfags.sign,609 clfags.alt,610 true,611 );612 }613 ConvTypeV::Shorter => {614 let value = f64::from_untyped(value.clone())?;615 let exponent = if value == 0.0 {616 0.0617 } else {618 value.abs().log10().floor()619 };620 if exponent < -4.0 || exponent >= f64::from(fpprec) {621 render_float_sci(622 &mut tmp_out,623 value,624 padding,625 fpprec - 1,626 clfags.blank,627 clfags.sign,628 clfags.alt,629 clfags.alt,630 code.caps,631 );632 } else {633 let digits_before_pt = 1.max(exponent as u16 + 1);634 render_float(635 &mut tmp_out,636 value,637 padding,638 fpprec - digits_before_pt,639 clfags.blank,640 clfags.sign,641 clfags.alt,642 clfags.alt,643 );644 }645 }646 ConvTypeV::Char => match value.clone() {647 Val::Num(n) => {648 let n = n.get();649 tmp_out.push(650 std::char::from_u32(n as u32)651 .ok_or_else(|| InvalidUnicodeCodepointGot(n as u32))?,652 );653 }654 Val::Str(s) => {655 let s = s.into_flat();656 if s.chars().count() != 1 {657 bail!("%c expected 1 char string, got {}", s.chars().count());658 }659 tmp_out.push_str(&s);660 }661 _ => {662 bail!(TypeMismatch(663 "%c requires number/string",664 vec![ValType::Num, ValType::Str],665 value.value_type(),666 ));667 }668 },669 ConvTypeV::Percent => tmp_out.push('%'),670 }671672 let padding = width.saturating_sub(tmp_out.len() as u16);673674 if !clfags.left {675 for _ in 0..padding {676 out.push(' ');677 }678 }679 out.push_str(&tmp_out);680 if clfags.left {681 for _ in 0..padding {682 out.push(' ');683 }684 }685686 Ok(())687}688689pub fn format_arr(str: &str, mut values: &[Val]) -> Result<String> {690 let codes = parse_codes(str)?;691 let mut out = String::new();692 let value_count = values.len();693694 for code in codes {695 match code {696 Element::String(s) => {697 out.push_str(s);698 }699 Element::Code(c) => {700 let width = match c.width {701 Width::Star => {702 if values.is_empty() {703 bail!(NotEnoughValues);704 }705 let value = &values[0];706 values = &values[1..];707 u16::from_untyped(value.clone())?708 }709 Width::Fixed(n) => n,710 };711 let precision = match c.precision {712 Some(Width::Star) => {713 if values.is_empty() {714 bail!(NotEnoughValues);715 }716 let value = &values[0];717 values = &values[1..];718 Some(u16::from_untyped(value.clone())?)719 }720 Some(Width::Fixed(n)) => Some(n),721 None => None,722 };723724 // %% should not consume a value725 let value = if c.convtype == ConvTypeV::Percent {726 &Val::Null727 } else {728 if values.is_empty() {729 bail!(NotEnoughValues);730 }731 let value = &values[0];732 values = &values[1..];733 value734 };735736 format_code(&mut out, value, &c, width, precision)?;737 }738 }739 }740741 if !values.is_empty() {742 bail!(743 "too many values to format, expected {value_count}, got {}",744 value_count + values.len()745 )746 }747748 Ok(out)749}750751fn get_dotted_field(obj: ObjValue, field: &str) -> Result<Val> {752 let mut current = Val::Obj(obj);753 let mut name_offset = 0;754 for component in field.split('.') {755 let end_offset = name_offset + component.len();756 current = if let Val::Obj(obj) = current {757 if let Some(value) = obj.get(component.into())? {758 value759 } else {760 let current = &field[name_offset..end_offset];761 let full = &field[..name_offset];762 let found = Box::new(suggest_object_fields(&obj, current.into()));763 bail!(SubfieldNotFound {764 current: current.into(),765 full: full.into(),766 found,767 })768 }769 } else {770 // No underflow may happen, initially we always start with an object771 let subfield = &field[..name_offset - 1];772 bail!(SubfieldDidntYieldAnObject(773 subfield.into(),774 current.value_type()775 ));776 };777 name_offset = end_offset + 1;778 }779 Ok(current)780}781782pub fn format_obj(str: &str, values: &ObjValue) -> Result<String> {783 let codes = parse_codes(str)?;784 let mut out = String::new();785786 for code in codes {787 match code {788 Element::String(s) => {789 out.push_str(s);790 }791 Element::Code(c) => {792 // TODO: Operate on ref793 let f: IStr = c.mkey.into();794 let width = match c.width {795 Width::Star => {796 bail!(CannotUseStarWidthWithObject);797 }798 Width::Fixed(n) => n,799 };800 let precision = match c.precision {801 Some(Width::Star) => {802 bail!(CannotUseStarWidthWithObject);803 }804 Some(Width::Fixed(n)) => Some(n),805 None => None,806 };807808 let value = if c.convtype == ConvTypeV::Percent {809 Val::Null810 } else {811 if f.is_empty() {812 bail!(MappingKeysRequired);813 }814 if let Some(v) = values.get(f.clone())? {815 v816 } else {817 get_dotted_field(values.clone(), &f)?818 }819 };820821 format_code(&mut out, &value, &c, width, precision)?;822 }823 }824 }825826 Ok(out)827}828829#[cfg(test)]830pub mod test_format {831 use super::*;832 use crate::val::NumValue;833834 #[test]835 fn parse() {836 assert_eq!(837 parse_codes(838 "How much error budget is left looking at our %.3f%% availability gurantees?"839 )840 .unwrap()841 .len(),842 4843 );844 }845846 fn num(v: f64) -> Val {847 Val::Num(NumValue::new(v).expect("finite"))848 }849850 #[test]851 fn octals() {852 assert_eq!(format_arr("%#o", &[num(8.0)]).unwrap(), "010");853 assert_eq!(format_arr("%#4o", &[num(8.0)]).unwrap(), " 010");854 assert_eq!(format_arr("%4o", &[num(8.0)]).unwrap(), " 10");855 assert_eq!(format_arr("%04o", &[num(8.0)]).unwrap(), "0010");856 assert_eq!(format_arr("%+4o", &[num(8.0)]).unwrap(), " +10");857 assert_eq!(format_arr("%+04o", &[num(8.0)]).unwrap(), "+010");858 assert_eq!(format_arr("%-4o", &[num(8.0)]).unwrap(), "10 ");859 assert_eq!(format_arr("%+-4o", &[num(8.0)]).unwrap(), "+10 ");860 assert_eq!(format_arr("%+-04o", &[num(8.0)]).unwrap(), "+10 ");861 }862863 #[test]864 fn percent_doesnt_consumes_values() {865 assert_eq!(866 format_arr(867 "How much error budget is left looking at our %.3f%% availability gurantees?",868 &[num(4.0)]869 )870 .unwrap(),871 "How much error budget is left looking at our 4.000% availability gurantees?"872 );873 }874}crates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -2,6 +2,8 @@
use jrsonnet_gcmodule::Trace;
use jrsonnet_interner::{IBytes, IStr};
+use jrsonnet_ir::NumValue;
+pub use jrsonnet_ir::{MAX_SAFE_INTEGER, MIN_SAFE_INTEGER};
use jrsonnet_types::{ComplexValType, ValType};
use crate::{
@@ -10,7 +12,7 @@
bail,
function::FuncVal,
typed::CheckType,
- val::{IndexableVal, NumValue, StrValue, ThunkMapper},
+ val::{IndexableVal, StrValue, ThunkMapper},
};
#[doc(hidden)]
@@ -219,11 +221,6 @@
Ok(inner.map(<ThunkFromUntyped<T>>::default()))
}
}
-
-#[expect(clippy::cast_precision_loss, reason = "checked to not overflow")]
-pub const MAX_SAFE_INTEGER: f64 = ((1u64 << (f64::MANTISSA_DIGITS)) - 1) as f64;
-#[expect(clippy::cast_precision_loss, reason = "checked to not overflow")]
-pub const MIN_SAFE_INTEGER: f64 = (-((1i64 << (f64::MANTISSA_DIGITS)) - 1)) as f64;
macro_rules! impl_int {
($($ty:ty)*) => {$(
crates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -5,7 +5,6 @@
marker::PhantomData,
mem::replace,
num::NonZeroU32,
- ops::Deref,
rc::Rc,
};
@@ -14,16 +13,15 @@
pub use jrsonnet_macros::Thunk;
use jrsonnet_types::ValType;
use rustc_hash::FxHashMap;
-use thiserror::Error;
pub use crate::arr::{ArrValue, ArrayLike};
use crate::{
- ObjValue, Result, SupThis, Unbound, WeakSupThis, bail,
+ NumValue, ObjValue, Result, SupThis, Unbound, WeakSupThis, bail,
error::{Error, ErrorKind::*},
function::FuncVal,
gc::WithCapacityExt as _,
manifest::{ManifestFormat, ToStringFormat},
- typed::{BoundedUsize, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER},
+ typed::BoundedUsize,
};
pub trait ThunkValue: Trace {
@@ -439,134 +437,6 @@
let a = self.clone().into_flat();
let b = other.clone().into_flat();
a.cmp(&b)
- }
-}
-
-/// Represents jsonnet number
-/// Jsonnet numbers are finite f64, with NaNs disallowed
-#[derive(Trace, Clone, Copy)]
-#[repr(transparent)]
-pub struct NumValue(f64);
-impl NumValue {
- /// Creates a [`NumValue`], if value is finite and not NaN
- pub fn new(v: f64) -> Option<Self> {
- if !v.is_finite() {
- return None;
- }
- Some(Self(v))
- }
- #[inline]
- pub const fn get(&self) -> f64 {
- self.0
- }
- pub(crate) fn truncate_for_bitwise(self) -> Result<i64> {
- if self.0 < MIN_SAFE_INTEGER || self.0 > MAX_SAFE_INTEGER {
- bail!("numberic value outside of safe integer range for bitwise operation");
- }
- #[expect(clippy::cast_possible_truncation, reason = "intended")]
- Ok(self.0 as i64)
- }
-}
-impl PartialEq for NumValue {
- fn eq(&self, other: &Self) -> bool {
- self.0 == other.0
- }
-}
-impl Eq for NumValue {}
-impl Ord for NumValue {
- #[inline]
- fn cmp(&self, other: &Self) -> Ordering {
- // Can't use `total_cmp`: its behavior for `-0` and `0`
- // is not following wanted.
- unsafe { self.0.partial_cmp(&other.0).unwrap_unchecked() }
- }
-}
-impl PartialOrd for NumValue {
- #[inline]
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
- }
-}
-impl Debug for NumValue {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- Debug::fmt(&self.0, f)
- }
-}
-impl Display for NumValue {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- Display::fmt(&self.0, f)
- }
-}
-impl Deref for NumValue {
- type Target = f64;
-
- #[inline]
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-macro_rules! impl_num {
- ($($ty:ty),+) => {$(
- impl From<$ty> for NumValue {
- #[inline]
- fn from(value: $ty) -> Self {
- Self(value.into())
- }
- }
- )+};
-}
-impl_num!(i8, u8, i16, u16, i32, u32);
-
-#[derive(Clone, Copy, Debug, Error, Trace)]
-pub enum ConvertNumValueError {
- #[error("overflow")]
- Overflow,
- #[error("underflow")]
- Underflow,
- #[error("non-finite")]
- NonFinite,
-}
-impl From<ConvertNumValueError> for Error {
- fn from(e: ConvertNumValueError) -> Self {
- Self::new(e.into())
- }
-}
-
-macro_rules! impl_try_num {
- ($($ty:ty),+) => {$(
- impl TryFrom<$ty> for NumValue {
- type Error = ConvertNumValueError;
- #[inline]
- fn try_from(value: $ty) -> Result<Self, ConvertNumValueError> {
- #[expect(clippy::cast_precision_loss, reason = "precision loss is explicitly handled")]
- let value = value as f64;
- if value < MIN_SAFE_INTEGER {
- return Err(ConvertNumValueError::Underflow)
- } else if value > MAX_SAFE_INTEGER {
- return Err(ConvertNumValueError::Overflow)
- }
- // Number is finite.
- Ok(Self(value))
- }
- }
- )+};
-}
-impl_try_num!(usize, isize, i64, u64);
-
-impl TryFrom<f64> for NumValue {
- type Error = ConvertNumValueError;
-
- #[inline]
- fn try_from(value: f64) -> Result<Self, Self::Error> {
- Self::new(value).ok_or(ConvertNumValueError::NonFinite)
- }
-}
-impl TryFrom<f32> for NumValue {
- type Error = ConvertNumValueError;
-
- #[inline]
- fn try_from(value: f32) -> Result<Self, Self::Error> {
- Self::new(f64::from(value)).ok_or(ConvertNumValueError::NonFinite)
}
}
crates/jrsonnet-ir-parser/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-ir-parser/src/lib.rs
+++ b/crates/jrsonnet-ir-parser/src/lib.rs
@@ -4,8 +4,8 @@
use jrsonnet_ir::{
ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BinaryOpType, BindSpec, CompSpec, Destruct, Expr,
ExprParam, ExprParams, FieldMember, FieldName, ForSpecData, IStr, IfElse, IfSpecData,
- ImportKind, IndexPart, LiteralType, Member, ObjBody, ObjComp, ObjMembers, Slice, SliceDesc,
- Source, Span, Spanned, UnaryOpType, Visibility, unescape,
+ ImportKind, IndexPart, LiteralType, Member, NumValue, ObjBody, ObjComp, ObjMembers, Slice,
+ SliceDesc, Source, Span, Spanned, UnaryOpType, Visibility, unescape,
};
use jrsonnet_lexer::{Lexeme, Lexer, Span as LexSpan, SyntaxKind, T, collect_lexed_str_block};
@@ -202,17 +202,21 @@
)
}
-fn parse_number(p: &mut Parser<'_>) -> Result<f64> {
+fn parse_number(p: &mut Parser<'_>) -> Result<NumValue> {
let text = p.text();
let n: f64 = text
.replace('_', "")
.parse()
.map_err(|_| p.error(format!("invalid number literal: {text}")))?;
- if !n.is_finite() {
- return Err(p.error("numbers are finite".into()));
- }
+
+ let v = match NumValue::try_from(n) {
+ Ok(v) => v,
+ Err(e) => return Err(p.error(format!("invalid number value: {e}"))),
+ };
+
p.eat_any();
- Ok(n)
+
+ Ok(v)
}
fn ident(p: &mut Parser<'_>) -> Result<IStr> {
crates/jrsonnet-ir/Cargo.tomldiffbeforeafterboth--- a/crates/jrsonnet-ir/Cargo.toml
+++ b/crates/jrsonnet-ir/Cargo.toml
@@ -19,6 +19,7 @@
static_assertions.workspace = true
peg.workspace = true
+thiserror.workspace = true
[dev-dependencies]
insta.workspace = true
crates/jrsonnet-ir/src/expr.rsdiffbeforeafterboth--- a/crates/jrsonnet-ir/src/expr.rs
+++ b/crates/jrsonnet-ir/src/expr.rs
@@ -8,6 +8,7 @@
use jrsonnet_interner::IStr;
use crate::{
+ NumValue,
function::{FunctionSignature, ParamDefault, ParamName, ParamParse},
source::Source,
};
@@ -398,7 +399,7 @@
/// String value: "hello"
Str(IStr),
/// Number: 1, 2.0, 2e+20
- Num(f64),
+ Num(NumValue),
/// Variable name: test
Var(Spanned<IStr>),
crates/jrsonnet-ir/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-ir/src/lib.rs
+++ b/crates/jrsonnet-ir/src/lib.rs
@@ -1,7 +1,10 @@
#![allow(clippy::redundant_closure_call, clippy::derive_partial_eq_without_eq)]
mod expr;
+use std::{cmp::Ordering, fmt, ops::Deref};
+
pub use expr::*;
+use jrsonnet_gcmodule::Acyclic;
pub use jrsonnet_interner::IStr;
pub mod function;
mod location;
@@ -14,3 +17,134 @@
Source, SourceDefaultIgnoreJpath, SourceDirectory, SourceFifo, SourceFile, SourcePath,
SourcePathT, SourceVirtual,
};
+
+// It seels to be a wrong place for this kind of stuff, but as it would also be used for static analysis and
+// is already wanted for NumValue, I don't know a better place.
+#[expect(clippy::cast_precision_loss, reason = "checked to not overflow")]
+pub const MAX_SAFE_INTEGER: f64 = ((1u64 << (f64::MANTISSA_DIGITS)) - 1) as f64;
+#[expect(clippy::cast_precision_loss, reason = "checked to not overflow")]
+pub const MIN_SAFE_INTEGER: f64 = (-((1i64 << (f64::MANTISSA_DIGITS)) - 1)) as f64;
+
+/// Represents jsonnet number
+/// Jsonnet numbers are finite f64, with NaNs disallowed
+#[derive(Acyclic, Clone, Copy)]
+pub struct NumValue(f64);
+impl NumValue {
+ /// Creates a [`NumValue`], if value is finite and not NaN
+ pub fn new(v: f64) -> Option<Self> {
+ if !v.is_finite() {
+ return None;
+ }
+ Some(Self(v))
+ }
+ #[inline]
+ pub const fn get(&self) -> f64 {
+ self.0
+ }
+ pub fn truncate_for_bitwise(self) -> Result<i64, ConvertNumValueError> {
+ if self.0 < MIN_SAFE_INTEGER || self.0 > MAX_SAFE_INTEGER {
+ return Err(ConvertNumValueError::BitwiseSafeRange);
+ }
+ #[expect(clippy::cast_possible_truncation, reason = "intended")]
+ Ok(self.0 as i64)
+ }
+}
+impl PartialEq for NumValue {
+ fn eq(&self, other: &Self) -> bool {
+ self.0 == other.0
+ }
+}
+impl Eq for NumValue {}
+impl Ord for NumValue {
+ #[inline]
+ fn cmp(&self, other: &Self) -> Ordering {
+ // Can't use `total_cmp`: its behavior for `-0` and `0`
+ // is not following wanted.
+ unsafe { self.0.partial_cmp(&other.0).unwrap_unchecked() }
+ }
+}
+impl PartialOrd for NumValue {
+ #[inline]
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+impl fmt::Debug for NumValue {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Debug::fmt(&self.0, f)
+ }
+}
+impl fmt::Display for NumValue {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Display::fmt(&self.0, f)
+ }
+}
+impl Deref for NumValue {
+ type Target = f64;
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+macro_rules! impl_num {
+ ($($ty:ty),+) => {$(
+ impl From<$ty> for NumValue {
+ #[inline]
+ fn from(value: $ty) -> Self {
+ Self(value.into())
+ }
+ }
+ )+};
+}
+impl_num!(i8, u8, i16, u16, i32, u32);
+
+#[derive(Clone, Copy, Debug, thiserror::Error, Acyclic)]
+pub enum ConvertNumValueError {
+ #[error("overflow")]
+ Overflow,
+ #[error("underflow")]
+ Underflow,
+ #[error("non-finite")]
+ NonFinite,
+ #[error("float out of safe int range")]
+ BitwiseSafeRange,
+}
+
+macro_rules! impl_try_num {
+ ($($ty:ty),+) => {$(
+ impl TryFrom<$ty> for NumValue {
+ type Error = ConvertNumValueError;
+ #[inline]
+ fn try_from(value: $ty) -> Result<Self, ConvertNumValueError> {
+ #[expect(clippy::cast_precision_loss, reason = "precision loss is explicitly handled")]
+ let value = value as f64;
+ if value < MIN_SAFE_INTEGER {
+ return Err(ConvertNumValueError::Underflow)
+ } else if value > MAX_SAFE_INTEGER {
+ return Err(ConvertNumValueError::Overflow)
+ }
+ // Number is finite.
+ Ok(Self(value))
+ }
+ }
+ )+};
+}
+impl_try_num!(usize, isize, i64, u64);
+
+impl TryFrom<f64> for NumValue {
+ type Error = ConvertNumValueError;
+
+ #[inline]
+ fn try_from(value: f64) -> Result<Self, Self::Error> {
+ Self::new(value).ok_or(ConvertNumValueError::NonFinite)
+ }
+}
+impl TryFrom<f32> for NumValue {
+ type Error = ConvertNumValueError;
+
+ #[inline]
+ fn try_from(value: f32) -> Result<Self, Self::Error> {
+ Self::new(f64::from(value)).ok_or(ConvertNumValueError::NonFinite)
+ }
+}
crates/jrsonnet-peg-parser/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-peg-parser/src/lib.rs
+++ b/crates/jrsonnet-peg-parser/src/lib.rs
@@ -4,8 +4,8 @@
use jrsonnet_ir::{
ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BindSpec, CompSpec, Destruct, DestructRest, Expr,
ExprParam, ExprParams, FieldMember, FieldName, ForSpecData, IStr, IfElse, IfSpecData,
- ImportKind, IndexPart, LiteralType, Member, ObjBody, ObjComp, ObjMembers, Slice, SliceDesc,
- Source, Span, Spanned, Visibility, unescape,
+ ImportKind, IndexPart, LiteralType, Member, NumValue, ObjBody, ObjComp, ObjMembers, Slice,
+ SliceDesc, Source, Span, Spanned, Visibility, unescape,
};
use peg::parser;
@@ -52,7 +52,7 @@
/// Sequence of digits
rule uint_str() -> &'input str = a:$(digit()+ ("_" digit()+)*) { a }
/// Number in scientific notation format
- rule number() -> f64 = quiet!{a:$(uint_str() ("." uint_str())? (['e'|'E'] (s:['+'|'-'])? uint_str())?) {? a.replace("_","").parse().map_err(|_| "<number>") }} / expected!("<number>")
+ rule number() -> f64 = quiet!{a:$(uint_str() ("." uint_str())? (['e'|'E'] (s:['+'|'-'])? uint_str())?) {? a.replace('_',"").parse().map_err(|_| "<number>") }} / expected!("<number>")
/// Reserved word followed by any non-alphanumberic
rule reserved() = ("assert" / "else" / "error" / "false" / "for" / "function" / "if" / "import" / "importstr" / "importbin" / "in" / "local" / "null" / "tailstrict" / "then" / "self" / "super" / "true") end_of_ident()
@@ -267,7 +267,7 @@
Expr::ArrComp(Rc::new(expr), specs)
}
pub rule number_expr(s: &ParserSettings) -> Expr
- = n:number() {? if n.is_finite() {
+ = n:number() {? if let Some(n) = NumValue::new(n) {
Ok(Expr::Num(n))
} else {
Err("!!!numbers are finite")
crates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/lib.rs
+++ b/crates/jrsonnet-stdlib/src/lib.rs
@@ -12,13 +12,12 @@
pub use encoding::*;
pub use hash::*;
use jrsonnet_evaluator::{
- ContextBuilder, IStr, ObjValue, ObjValueBuilder, Thunk, Val,
+ ContextBuilder, IStr, NumValue, ObjValue, ObjValueBuilder, Thunk, Val,
error::Result,
function::{CallLocation, FuncVal, builtin_id},
tla::TlaArg,
trace::PathResolver,
typed::SerializeTypedObj as _,
- val::NumValue,
};
use jrsonnet_gcmodule::{Acyclic, Cc, Trace};
use jrsonnet_ir::Source;
crates/jrsonnet-stdlib/src/operator.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/operator.rs
+++ b/crates/jrsonnet-stdlib/src/operator.rs
@@ -2,12 +2,12 @@
//! However, in our case we instead implement them in native, and implement native functions on top of core for backwards compatibility
use jrsonnet_evaluator::{
- IStr, Result, Val,
+ IStr, NumValue, Result, Val,
function::builtin,
operator::evaluate_mod_op,
stdlib::std_format,
typed::{Either, Either2},
- val::{NumValue, equals, primitive_equals},
+ val::{equals, primitive_equals},
};
#[builtin]