--- 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; --- 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 for Error { + fn from(e: ConvertNumValueError) -> Self { + Self::new(ErrorKind::ConvertNumValue(e)) + } +} impl From for Error { fn from(_value: Infallible) -> Self { --- 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, --- 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 { --- 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; --- a/crates/jrsonnet-evaluator/src/stdlib/format.rs +++ b/crates/jrsonnet-evaluator/src/stdlib/format.rs @@ -829,7 +829,7 @@ #[cfg(test)] pub mod test_format { use super::*; - use crate::val::NumValue; + use crate::NumValue; #[test] fn parse() { --- 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(>::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)*) => {$( --- 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 { - 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 { - 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 { - 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 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 { - #[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 for NumValue { - type Error = ConvertNumValueError; - - #[inline] - fn try_from(value: f64) -> Result { - Self::new(value).ok_or(ConvertNumValueError::NonFinite) - } -} -impl TryFrom for NumValue { - type Error = ConvertNumValueError; - - #[inline] - fn try_from(value: f32) -> Result { - Self::new(f64::from(value)).ok_or(ConvertNumValueError::NonFinite) } } --- 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 { +fn parse_number(p: &mut Parser<'_>) -> Result { 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 { --- 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 --- 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), --- 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 { + 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 { + 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 { + 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 { + #[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 for NumValue { + type Error = ConvertNumValueError; + + #[inline] + fn try_from(value: f64) -> Result { + Self::new(value).ok_or(ConvertNumValueError::NonFinite) + } +} +impl TryFrom for NumValue { + type Error = ConvertNumValueError; + + #[inline] + fn try_from(value: f32) -> Result { + Self::new(f64::from(value)).ok_or(ConvertNumValueError::NonFinite) + } +} --- 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(|_| "") }} / expected!("") + rule number() -> f64 = quiet!{a:$(uint_str() ("." uint_str())? (['e'|'E'] (s:['+'|'-'])? uint_str())?) {? a.replace('_',"").parse().map_err(|_| "") }} / expected!("") /// 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") --- 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; --- 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]