git.delta.rocks / jrsonnet / refs/commits / 7af406eaa740

difftreelog

feat return NumValue directly from parser

rlsuqplzYaroslav Bolyukin2026-04-25parent: #eee6c62.patch.diff
in: master

15 files changed

modifiedbindings/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;
 
modifiedcrates/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 {
modifiedcrates/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,
modifiedcrates/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 {
modifiedcrates/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;
modifiedcrates/jrsonnet-evaluator/src/stdlib/format.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/stdlib/format.rs
1//! 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}
modifiedcrates/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)*) => {$(
modifiedcrates/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)
 	}
 }
 
modifiedcrates/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> {
modifiedcrates/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
modifiedcrates/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>),
 
modifiedcrates/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)
+	}
+}
modifiedcrates/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")
modifiedcrates/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;
modifiedcrates/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]