git.delta.rocks / jrsonnet / refs/commits / 1bfba233fc03

difftreelog

refactor reenable clippy integer cast checks

slqpxoouYaroslav Bolyukin2026-04-04parent: #191649c.patch.diff
in: master

17 files changed

modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -122,11 +122,6 @@
 wildcard_imports = "allow"
 enum_glob_use = "allow"
 module_name_repetitions = "allow"
-# TODO: fix individual issues, however this works as intended almost everywhere
-cast_precision_loss = "allow"
-cast_possible_wrap = "allow"
-cast_possible_truncation = "allow"
-cast_sign_loss = "allow"
 # False positives
 # https://github.com/rust-lang/rust-clippy/issues/6902
 use_self = "allow"
modifiedcrates/jrsonnet-evaluator/src/arr/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/mod.rs
+++ b/crates/jrsonnet-evaluator/src/arr/mod.rs
@@ -128,7 +128,12 @@
 	#[must_use]
 	pub fn slice(self, index: Option<i32>, end: Option<i32>, step: Option<NonZeroU32>) -> Self {
 		let get_idx = |pos: Option<i32>, len: usize, default| match pos {
+			#[expect(
+				clippy::cast_sign_loss,
+				reason = "abs value is used, len is limited to u31"
+			)]
 			Some(v) if v < 0 => len.saturating_sub((-v) as usize),
+			#[expect(clippy::cast_sign_loss, reason = "abs value is used")]
 			Some(v) => (v as usize).min(len),
 			None => default,
 		};
@@ -142,7 +147,9 @@
 
 		Self::new(SliceArray {
 			inner: self,
+			#[expect(clippy::cast_possible_truncation, reason = "len is limited to u31")]
 			from: index as u32,
+			#[expect(clippy::cast_possible_truncation, reason = "len is limited to u31")]
 			to: end as u32,
 			step: step.get(),
 		})
modifiedcrates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -350,22 +350,26 @@
 	pub fn new_inclusive(start: i32, end: i32) -> Self {
 		Self { start, end }
 	}
+	#[expect(
+		clippy::cast_sign_loss,
+		reason = "the math is valid with wrapping, sign loss works as intended"
+	)]
+	fn size(&self) -> usize {
+		(self.end as usize)
+			.wrapping_sub(self.start as usize)
+			.wrapping_add(1)
+	}
 	fn range(&self) -> impl ExactSizeIterator<Item = i32> + DoubleEndedIterator {
-		WithExactSize(
-			self.start..=self.end,
-			(self.end as usize)
-				.wrapping_sub(self.start as usize)
-				.wrapping_add(1),
-		)
+		WithExactSize(self.start..=self.end, self.size())
 	}
 }
 
 impl ArrayLike for RangeArray {
 	fn len(&self) -> usize {
-		self.range().len()
+		self.size()
 	}
 	fn is_empty(&self) -> bool {
-		self.range().len() == 0
+		self.size() == 0
 	}
 
 	fn get(&self, index: usize) -> Result<Option<Val>> {
@@ -431,6 +435,10 @@
 	fn evaluate(&self, index: usize, value: Val) -> Result<Val> {
 		match &self.mapper {
 			ArrayMapper::Plain(f) => f.call(value),
+			#[expect(
+				clippy::cast_possible_truncation,
+				reason = "array len is limited to u31"
+			)]
 			ArrayMapper::WithIndex(f) => f.call(index as u32, value),
 		}
 	}
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -548,8 +548,18 @@
 							bail!(FractionalIndex)
 						}
 						if n < 0.0 {
-							bail!(ArrayBoundsError(n as isize, v.len()));
+							#[expect(
+								clippy::cast_possible_truncation,
+								reason = "it would be truncated anyway"
+							)]
+							let n = n as isize;
+							bail!(ArrayBoundsError(n, v.len()));
 						}
+						#[expect(
+							clippy::cast_possible_truncation,
+							clippy::cast_sign_loss,
+							reason = "n is checked postive"
+						)]
 						v.get(n as usize)?
 							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?
 					}
@@ -568,18 +578,29 @@
 							bail!(FractionalIndex)
 						}
 						if n < 0.0 {
-							bail!(ArrayBoundsError(n as isize, s.into_flat().chars().count()));
+							#[expect(
+								clippy::cast_possible_truncation,
+								reason = "it would be truncated anyway"
+							)]
+							let n = n as isize;
+							bail!(ArrayBoundsError(n, s.into_flat().chars().count()));
 						}
+						#[expect(
+							clippy::cast_sign_loss,
+							clippy::cast_possible_truncation,
+							reason = "n is positive, overflow will truncate as expected"
+						)]
+						let n = n as usize;
 						let v: IStr = s
 							.clone()
 							.into_flat()
 							.chars()
-							.skip(n as usize)
+							.skip(n)
 							.take(1)
 							.collect::<String>()
 							.into();
 						if v.is_empty() {
-							bail!(StringBoundsError(n as usize, s.into_flat().chars().count()))
+							bail!(StringBoundsError(n, s.into_flat().chars().count()))
 						}
 						StrValue::Flat(v)
 					}),
modifiedcrates/jrsonnet-evaluator/src/evaluate/operator.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/operator.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/operator.rs
@@ -20,7 +20,8 @@
 		(Plus, Num(n)) => Val::Num(*n),
 		(Minus, Num(n)) => Val::try_num(-n.get())?,
 		(Not, Bool(v)) => Bool(!v),
-		(BitNot, Num(n)) => Val::try_num(!(n.get() as i64) as f64)?,
+		#[expect(clippy::cast_precision_loss, reason = "as spec")]
+		(BitNot, Num(n)) => Val::try_num(!n.truncate_for_bitwise()? as f64)?,
 		(op, o) => bail!(UnaryOperatorDoesNotOperateOnType(op, o.value_type())),
 	})
 }
@@ -73,7 +74,17 @@
 pub fn evaluate_mul_op(a: &Val, b: &Val) -> Result<Val> {
 	use Val::*;
 	Ok(match (a, b) {
+		#[expect(
+			clippy::cast_possible_truncation,
+			clippy::cast_sign_loss,
+			reason = "should not be used with values too large, negative == 0"
+		)]
 		(Str(s), Num(c)) => Val::string(s.to_string().repeat(c.get() as usize)),
+		#[expect(
+			clippy::cast_possible_truncation,
+			clippy::cast_sign_loss,
+			reason = "should not be used with values too large"
+		)]
 		(Num(c), Str(s)) => Val::string(s.to_string().repeat(c.get() as usize)),
 
 		(Num(v1), Num(v2)) => Val::try_num(v1.get() * v2.get())?,
@@ -218,13 +229,28 @@
 		(a, Div, b) => evaluate_div_op(a, b)?,
 		(a, Mod, b) => evaluate_mod_op(a, b)?,
 
-		(Num(v1), BitAnd, Num(v2)) => {
+		(Num(v1), BitAnd, Num(v2)) =>
+		{
+			#[expect(
+				clippy::cast_precision_loss,
+				reason = "values are within safe integer ranges"
+			)]
 			Val::try_num((v1.truncate_for_bitwise()? & v2.truncate_for_bitwise()?) as f64)?
 		}
-		(Num(v1), BitOr, Num(v2)) => {
+		(Num(v1), BitOr, Num(v2)) =>
+		{
+			#[expect(
+				clippy::cast_precision_loss,
+				reason = "values are within safe integer ranges"
+			)]
 			Val::try_num((v1.truncate_for_bitwise()? | v2.truncate_for_bitwise()?) as f64)?
 		}
-		(Num(v1), BitXor, Num(v2)) => {
+		(Num(v1), BitXor, Num(v2)) =>
+		{
+			#[expect(
+				clippy::cast_precision_loss,
+				reason = "values are within safe integer ranges"
+			)]
 			Val::try_num((v1.truncate_for_bitwise()? ^ v2.truncate_for_bitwise()?) as f64)?
 		}
 		(Num(v1), Lhs, Num(v2)) => {
@@ -234,16 +260,28 @@
 			let base = v1.truncate_for_bitwise()?;
 			let exp = v2.truncate_for_bitwise()? % 64;
 
+			#[expect(clippy::cast_sign_loss, reason = "exp is positive")]
 			if exp >= 1 && base >= (1i64 << (63 - exp as u32)) {
 				bail!("left shift would overflow")
 			}
+			#[expect(
+				clippy::cast_precision_loss,
+				clippy::cast_sign_loss,
+				reason = "checked as original impl"
+			)]
 			Val::try_num(base.wrapping_shl(exp as u32) as f64)?
 		}
 		(Num(v1), Rhs, Num(v2)) => {
 			if v2.get() < 0.0 {
 				bail!("shift by negative exponent")
 			}
+			#[expect(
+				clippy::cast_sign_loss,
+				clippy::cast_possible_truncation,
+				reason = "checked as original impl"
+			)]
 			let exp = ((v2.get() as i64) & 63) as u32;
+			#[expect(clippy::cast_precision_loss, reason = "checked as upstream impl")]
 			Val::try_num(v1.truncate_for_bitwise()?.wrapping_shr(exp) as f64)?
 		}
 
modifiedcrates/jrsonnet-evaluator/src/integrations/serde.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/integrations/serde.rs
+++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs
@@ -69,12 +69,20 @@
 			where
 				E: de::Error,
 			{
+				#[expect(
+					clippy::cast_precision_loss,
+					reason = "this is how it works with stdlib functions"
+				)]
 				Ok(Val::Num(NumValue::new(v as f64).expect("no overflow")))
 			}
 			fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
 			where
 				E: de::Error,
 			{
+				#[expect(
+					clippy::cast_precision_loss,
+					reason = "this is how it works with stdlib functions"
+				)]
 				Ok(Val::Num(NumValue::new(v as f64).expect("no overflow")))
 			}
 
@@ -161,6 +169,10 @@
 			Self::Num(n) => {
 				let n = n.get();
 				if n.fract() == 0.0 {
+					#[expect(
+						clippy::cast_possible_truncation,
+						reason = "no correct implementation is possible here; expected"
+					)]
 					let n = n as i64;
 					serializer.serialize_i64(n)
 				} else {
modifiedcrates/jrsonnet-evaluator/src/obj/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/obj/mod.rs
+++ b/crates/jrsonnet-evaluator/src/obj/mod.rs
@@ -792,6 +792,8 @@
 			key,
 		})
 	}
+
+	#[allow(dead_code, reason = "used in object ...rest destructuring")]
 	pub(crate) fn as_standalone(&self) -> StandaloneSuperCore {
 		StandaloneSuperCore {
 			sup: CoreIdx {
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)]34use jrsonnet_gcmodule::Trace;5use jrsonnet_interner::IStr;6use jrsonnet_types::ValType;7use thiserror::Error;89use crate::{10	Error, ObjValue, Result, Val, bail,11	error::{ErrorKind::*, format_found, suggest_object_fields},12	typed::FromUntyped,13};1415#[derive(Debug, Clone, Error, Trace)]16pub enum FormatError {17	#[error("truncated format code")]18	TruncatedFormatCode,19	#[error("unrecognized conversion type: {0}")]20	UnrecognizedConversionType(char),2122	#[error("not enough values")]23	NotEnoughValues,2425	#[error("cannot use * width with object")]26	CannotUseStarWidthWithObject,27	#[error("mapping keys required")]28	MappingKeysRequired,29	#[error("no such format field: {0}")]30	NoSuchFormatField(IStr),3132	#[error("expected subfield <{0}> to be an object, got {1} instead")]33	SubfieldDidntYieldAnObject(IStr, ValType),34	#[error("subfield not found: <[{full}]{current}>{}", format_found(.found, "subfield"))]35	SubfieldNotFound {36		current: IStr,37		full: IStr,38		found: Box<Vec<IStr>>,39	},40}4142impl From<FormatError> for Error {43	fn from(e: FormatError) -> Self {44		Self::new(Format(e))45	}46}4748use FormatError::*;4950type ParseResult<'t, T> = std::result::Result<(T, &'t str), FormatError>;5152pub fn try_parse_mapping_key(str: &str) -> ParseResult<'_, &str> {53	if str.is_empty() {54		return Err(TruncatedFormatCode);55	}56	let bytes = str.as_bytes();57	if bytes[0] == b'(' {58		let mut i = 1;59		while i < bytes.len() {60			if bytes[i] == b')' {61				return Ok((&str[1..i], &str[i + 1..]));62			}63			i += 1;64		}65		Err(TruncatedFormatCode)66	} else {67		Ok(("", str))68	}69}7071#[cfg(test)]72pub mod tests_key {73	use super::*;7475	#[test]76	fn parse_key() {77		assert_eq!(78			try_parse_mapping_key("(hello ) world").unwrap(),79			("hello ", " world")80		);81		assert_eq!(try_parse_mapping_key("() world").unwrap(), ("", " world"));82		assert_eq!(try_parse_mapping_key(" world").unwrap(), ("", " world"));83		assert_eq!(84			try_parse_mapping_key(" () world").unwrap(),85			("", " () world")86		);87	}8889	#[test]90	#[should_panic = "TruncatedFormatCode"]91	fn parse_key_missing_start() {92		try_parse_mapping_key("").unwrap();93	}9495	#[test]96	#[should_panic = "TruncatedFormatCode"]97	fn parse_key_missing_end() {98		try_parse_mapping_key("(   ").unwrap();99	}100}101102#[allow(clippy::struct_excessive_bools)]103#[derive(Default, Debug)]104pub struct CFlags {105	pub alt: bool,106	pub zero: bool,107	pub left: bool,108	pub blank: bool,109	pub sign: bool,110}111112pub fn try_parse_cflags(str: &str) -> ParseResult<'_, CFlags> {113	if str.is_empty() {114		return Err(TruncatedFormatCode);115	}116	let bytes = str.as_bytes();117	let mut i = 0;118	let mut out = CFlags::default();119	loop {120		if bytes.len() == i {121			return Err(TruncatedFormatCode);122		}123		match bytes[i] {124			b'#' => out.alt = true,125			b'0' => out.zero = true,126			b'-' => out.left = true,127			b' ' => out.blank = true,128			b'+' => out.sign = true,129			_ => break,130		}131		i += 1;132	}133	Ok((out, &str[i..]))134}135136#[derive(Debug, PartialEq, Eq)]137pub enum Width {138	Star,139	Fixed(u16),140}141pub fn try_parse_field_width(str: &str) -> ParseResult<'_, Width> {142	if str.is_empty() {143		return Err(TruncatedFormatCode);144	}145	let bytes = str.as_bytes();146	if bytes[0] == b'*' {147		return Ok((Width::Star, &str[1..]));148	}149	let mut out: u16 = 0;150	let mut digits = 0;151	while let Some(digit) = (bytes[digits] as char).to_digit(10) {152		out *= 10;153		out += digit as u16;154		digits += 1;155		if digits == bytes.len() {156			return Err(TruncatedFormatCode);157		}158	}159	Ok((Width::Fixed(out), &str[digits..]))160}161162pub fn try_parse_precision(str: &str) -> ParseResult<'_, Option<Width>> {163	if str.is_empty() {164		return Err(TruncatedFormatCode);165	}166	let bytes = str.as_bytes();167	if bytes[0] == b'.' {168		try_parse_field_width(&str[1..]).map(|(r, s)| (Some(r), s))169	} else {170		Ok((None, str))171	}172}173174// Only skips175pub fn try_parse_length_modifier(str: &str) -> ParseResult<'_, ()> {176	if str.is_empty() {177		return Err(TruncatedFormatCode);178	}179	let bytes = str.as_bytes();180	let mut idx = 0;181	while bytes[idx] == b'h' || bytes[idx] == b'l' || bytes[idx] == b'L' {182		idx += 1;183		if bytes.len() == idx {184			return Err(TruncatedFormatCode);185		}186	}187	Ok(((), &str[idx..]))188}189190#[derive(Debug, PartialEq, Eq)]191pub enum ConvTypeV {192	Decimal,193	Octal,194	Hexadecimal,195	Scientific,196	Float,197	Shorter,198	Char,199	String,200	Percent,201}202pub struct ConvType {203	v: ConvTypeV,204	caps: bool,205}206207pub fn parse_conversion_type(str: &str) -> ParseResult<'_, ConvType> {208	if str.is_empty() {209		return Err(TruncatedFormatCode);210	}211212	let code = str.as_bytes()[0];213	let v: (ConvTypeV, bool) = match code {214		b'd' | b'i' | b'u' => (ConvTypeV::Decimal, false),215		b'o' => (ConvTypeV::Octal, false),216		b'x' => (ConvTypeV::Hexadecimal, false),217		b'X' => (ConvTypeV::Hexadecimal, true),218		b'e' => (ConvTypeV::Scientific, false),219		b'E' => (ConvTypeV::Scientific, true),220		b'f' => (ConvTypeV::Float, false),221		b'F' => (ConvTypeV::Float, true),222		b'g' => (ConvTypeV::Shorter, false),223		b'G' => (ConvTypeV::Shorter, true),224		b'c' => (ConvTypeV::Char, false),225		b's' => (ConvTypeV::String, false),226		b'%' => (ConvTypeV::Percent, false),227		c => return Err(UnrecognizedConversionType(c as char)),228	};229230	Ok((ConvType { v: v.0, caps: v.1 }, &str[1..]))231}232233#[derive(Debug)]234pub struct Code<'s> {235	mkey: &'s str,236	cflags: CFlags,237	width: Width,238	precision: Option<Width>,239	convtype: ConvTypeV,240	caps: bool,241}242pub fn parse_code(str: &str) -> ParseResult<'_, Code<'_>> {243	if str.is_empty() {244		return Err(TruncatedFormatCode);245	}246	let (mkey, str) = try_parse_mapping_key(str)?;247	let (cflags, str) = try_parse_cflags(str)?;248	let (width, str) = try_parse_field_width(str)?;249	let (precision, str) = try_parse_precision(str)?;250	let ((), str) = try_parse_length_modifier(str)?;251	let (convtype, str) = parse_conversion_type(str)?;252253	Ok((254		Code {255			mkey,256			cflags,257			width,258			precision,259			convtype: convtype.v,260			caps: convtype.caps,261		},262		str,263	))264}265266#[derive(Debug)]267pub enum Element<'s> {268	String(&'s str),269	Code(Code<'s>),270}271pub fn parse_codes(mut str: &str) -> Result<Vec<Element<'_>>> {272	let mut bytes = str.as_bytes();273	let mut out = vec![];274	let mut offset = 0;275276	loop {277		while offset != bytes.len() && bytes[offset] != b'%' {278			offset += 1;279		}280		if offset != 0 {281			out.push(Element::String(&str[0..offset]));282		}283		if offset == bytes.len() {284			return Ok(out);285		}286		str = &str[offset + 1..];287		let code;288		(code, str) = parse_code(str)?;289		bytes = str.as_bytes();290		offset = 0;291292		out.push(Element::Code(code));293	}294}295296const NUMBERS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";297298#[inline]299#[allow(clippy::fn_params_excessive_bools)]300pub fn render_integer(301	out: &mut String,302	neg: bool,303	iv: f64,304	padding: u16,305	precision: u16,306	blank: bool,307	sign: bool,308	radix: i64,309	zero_prefix: &str,310	prefix_in_padding: bool,311	caps: bool,312) {313	debug_assert!(iv >= 0.0, "render_integer receives sign using arg");314	let iv = iv.floor() as i64;315	// Digit char indexes in reverse order, i.e316	// for radix = 16 and n = 12f: [15, 2, 1]317	let digits = if iv == 0 {318		vec![0u8]319	} else {320		let mut v = iv.abs();321		let mut nums = Vec::with_capacity(1);322		while v != 0 {323			nums.push((v % radix) as u8);324			v /= radix;325		}326		nums327	};328	#[allow(clippy::bool_to_int_with_if)]329	let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 });330331	let pref_len = zero_prefix.len() as u16;332	let zp2 = zp333		.saturating_sub(if prefix_in_padding { 0 } else { pref_len })334		.max(precision)335		.saturating_sub(if prefix_in_padding { pref_len } else { 0 } + digits.len() as u16);336337	if neg {338		out.push('-');339	} else if sign {340		out.push('+');341	} else if blank {342		out.push(' ');343	}344345	out.reserve(zp2 as usize);346	if iv != 0 {347		out.push_str(zero_prefix);348	}349	for _ in 0..zp2 {350		out.push('0');351	}352353	for digit in digits.into_iter().rev() {354		let ch = NUMBERS[digit as usize] as char;355		out.push(if caps { ch.to_ascii_uppercase() } else { ch });356	}357}358359pub fn render_decimal(360	out: &mut String,361	neg: bool,362	iv: f64,363	padding: u16,364	precision: u16,365	blank: bool,366	sign: bool,367) {368	render_integer(369		out, neg, iv, padding, precision, blank, sign, 10, "", false, false,370	);371}372#[allow(clippy::fn_params_excessive_bools)]373pub fn render_octal(374	out: &mut String,375	neg: bool,376	iv: f64,377	padding: u16,378	precision: u16,379	alt: bool,380	blank: bool,381	sign: bool,382) {383	render_integer(384		out,385		neg,386		iv,387		padding,388		precision,389		blank,390		sign,391		8,392		if alt && iv != 0.0 { "0" } else { "" },393		true,394		false,395	);396}397398#[allow(clippy::fn_params_excessive_bools)]399pub fn render_hexadecimal(400	out: &mut String,401	iv: f64,402	padding: u16,403	precision: u16,404	alt: bool,405	blank: bool,406	sign: bool,407	caps: bool,408) {409	render_integer(410		out,411		iv < 0.0,412		iv.abs(),413		padding,414		precision,415		blank,416		sign,417		16,418		match (alt, caps) {419			(true, true) => "0X",420			(true, false) => "0x",421			(false, _) => "",422		},423		false,424		caps,425	);426}427428#[allow(clippy::fn_params_excessive_bools)]429pub fn render_float(430	out: &mut String,431	n: f64,432	mut padding: u16,433	precision: u16,434	blank: bool,435	sign: bool,436	ensure_pt: bool,437	trailing: bool,438) {439	// Represent the rounded number as an integer * 1/10**prec.440	// Note that it can also be equal to 10**prec and we'll need to carry441	// over to the wholes.  We operate on the absolute numbers, so that we442	// don't have trouble with the rounding direction.443	let denominator = 10.0f64.powi(i32::from(precision));444	let numerator = n.abs().mul_add(denominator, 0.5);445	let whole = (numerator / denominator).floor();446	let frac = numerator.floor() % denominator;447448	#[allow(clippy::bool_to_int_with_if)]449	let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 };450	padding = padding.saturating_sub(dot_size + precision);451	render_decimal(out, n < 0.0, whole, padding, 0, blank, sign);452	if precision == 0 {453		if ensure_pt {454			out.push('.');455		}456		return;457	}458	if trailing || frac > 0.0 {459		out.push('.');460		let mut frac_str = String::new();461		render_decimal(&mut frac_str, false, frac, precision, 0, false, false);462		let mut trim = frac_str.len();463		if !trailing {464			for b in frac_str.as_bytes().iter().rev() {465				if *b == b'0' {466					trim -= 1;467				} else {468					break;469				}470			}471		}472		out.push_str(&frac_str[..trim]);473	} else if ensure_pt {474		out.push('.');475	}476}477478#[allow(clippy::fn_params_excessive_bools)]479pub fn render_float_sci(480	out: &mut String,481	n: f64,482	mut padding: u16,483	precision: u16,484	blank: bool,485	sign: bool,486	ensure_pt: bool,487	trailing: bool,488	caps: bool,489) {490	let exponent = if n == 0.0 {491		0.0492	} else {493		n.abs().log10().floor()494	};495496	let mantissa = if exponent as i16 == -324 {497		n * 10.0 / 10.0_f64.powf(exponent + 1.0)498	} else {499		n / 10.0_f64.powf(exponent)500	};501	let mut exponent_str = String::new();502	render_decimal(503		&mut exponent_str,504		exponent < 0.0,505		exponent.abs(),506		3,507		0,508		false,509		true,510	);511512	// +1 for e513	padding = padding.saturating_sub(exponent_str.len() as u16 + 1);514515	render_float(516		out, mantissa, padding, precision, blank, sign, ensure_pt, trailing,517	);518	out.push(if caps { 'E' } else { 'e' });519	out.push_str(&exponent_str);520}521522#[allow(clippy::too_many_lines)]523pub fn format_code(524	out: &mut String,525	value: &Val,526	code: &Code<'_>,527	width: u16,528	precision: Option<u16>,529) -> Result<()> {530	let clfags = &code.cflags;531	let (fpprec, iprec) = precision.map_or((6, 0), |v| (v, v));532	let padding = if clfags.zero && !clfags.left {533		width534	} else {535		0536	};537538	// TODO: If left padded, can optimize by writing directly to out539	let mut tmp_out = String::new();540541	match code.convtype {542		ConvTypeV::String => tmp_out.push_str(&value.clone().to_string()?),543		ConvTypeV::Decimal => {544			let value = f64::from_untyped(value.clone())?;545			render_decimal(546				&mut tmp_out,547				value <= -1.0,548				value.abs(),549				padding,550				iprec,551				clfags.blank,552				clfags.sign,553			);554		}555		ConvTypeV::Octal => {556			let value = f64::from_untyped(value.clone())?;557			render_octal(558				&mut tmp_out,559				value <= -1.0,560				value.abs(),561				padding,562				iprec,563				clfags.alt,564				clfags.blank,565				clfags.sign,566			);567		}568		ConvTypeV::Hexadecimal => {569			let value = f64::from_untyped(value.clone())?;570			render_hexadecimal(571				&mut tmp_out,572				value,573				padding,574				iprec,575				clfags.alt,576				clfags.blank,577				clfags.sign,578				code.caps,579			);580		}581		ConvTypeV::Scientific => {582			let value = f64::from_untyped(value.clone())?;583			render_float_sci(584				&mut tmp_out,585				value,586				padding,587				fpprec,588				clfags.blank,589				clfags.sign,590				clfags.alt,591				true,592				code.caps,593			);594		}595		ConvTypeV::Float => {596			let value = f64::from_untyped(value.clone())?;597			render_float(598				&mut tmp_out,599				value,600				padding,601				fpprec,602				clfags.blank,603				clfags.sign,604				clfags.alt,605				true,606			);607		}608		ConvTypeV::Shorter => {609			let value = f64::from_untyped(value.clone())?;610			let exponent = if value == 0.0 {611				0.0612			} else {613				value.abs().log10().floor()614			};615			if exponent < -4.0 || exponent >= f64::from(fpprec) {616				render_float_sci(617					&mut tmp_out,618					value,619					padding,620					fpprec - 1,621					clfags.blank,622					clfags.sign,623					clfags.alt,624					clfags.alt,625					code.caps,626				);627			} else {628				let digits_before_pt = 1.max(exponent as u16 + 1);629				render_float(630					&mut tmp_out,631					value,632					padding,633					fpprec - digits_before_pt,634					clfags.blank,635					clfags.sign,636					clfags.alt,637					clfags.alt,638				);639			}640		}641		ConvTypeV::Char => match value.clone() {642			Val::Num(n) => {643				let n = n.get();644				tmp_out.push(645					std::char::from_u32(n as u32)646						.ok_or_else(|| InvalidUnicodeCodepointGot(n as u32))?,647				);648			}649			Val::Str(s) => {650				let s = s.into_flat();651				if s.chars().count() != 1 {652					bail!("%c expected 1 char string, got {}", s.chars().count());653				}654				tmp_out.push_str(&s);655			}656			_ => {657				bail!(TypeMismatch(658					"%c requires number/string",659					vec![ValType::Num, ValType::Str],660					value.value_type(),661				));662			}663		},664		ConvTypeV::Percent => tmp_out.push('%'),665	}666667	let padding = width.saturating_sub(tmp_out.len() as u16);668669	if !clfags.left {670		for _ in 0..padding {671			out.push(' ');672		}673	}674	out.push_str(&tmp_out);675	if clfags.left {676		for _ in 0..padding {677			out.push(' ');678		}679	}680681	Ok(())682}683684pub fn format_arr(str: &str, mut values: &[Val]) -> Result<String> {685	let codes = parse_codes(str)?;686	let mut out = String::new();687	let value_count = values.len();688689	for code in codes {690		match code {691			Element::String(s) => {692				out.push_str(s);693			}694			Element::Code(c) => {695				let width = match c.width {696					Width::Star => {697						if values.is_empty() {698							bail!(NotEnoughValues);699						}700						let value = &values[0];701						values = &values[1..];702						u16::from_untyped(value.clone())?703					}704					Width::Fixed(n) => n,705				};706				let precision = match c.precision {707					Some(Width::Star) => {708						if values.is_empty() {709							bail!(NotEnoughValues);710						}711						let value = &values[0];712						values = &values[1..];713						Some(u16::from_untyped(value.clone())?)714					}715					Some(Width::Fixed(n)) => Some(n),716					None => None,717				};718719				// %% should not consume a value720				let value = if c.convtype == ConvTypeV::Percent {721					&Val::Null722				} else {723					if values.is_empty() {724						bail!(NotEnoughValues);725					}726					let value = &values[0];727					values = &values[1..];728					value729				};730731				format_code(&mut out, value, &c, width, precision)?;732			}733		}734	}735736	if !values.is_empty() {737		bail!(738			"too many values to format, expected {value_count}, got {}",739			value_count + values.len()740		)741	}742743	Ok(out)744}745746fn get_dotted_field(obj: ObjValue, field: &str) -> Result<Val> {747	let mut current = Val::Obj(obj);748	let mut name_offset = 0;749	for component in field.split('.') {750		let end_offset = name_offset + component.len();751		current = if let Val::Obj(obj) = current {752			if let Some(value) = obj.get(component.into())? {753				value754			} else {755				let current = &field[name_offset..end_offset];756				let full = &field[..name_offset];757				let found = Box::new(suggest_object_fields(&obj, current.into()));758				bail!(SubfieldNotFound {759					current: current.into(),760					full: full.into(),761					found,762				})763			}764		} else {765			// No underflow may happen, initially we always start with an object766			let subfield = &field[..name_offset - 1];767			bail!(SubfieldDidntYieldAnObject(768				subfield.into(),769				current.value_type()770			));771		};772		name_offset = end_offset + 1;773	}774	Ok(current)775}776777pub fn format_obj(str: &str, values: &ObjValue) -> Result<String> {778	let codes = parse_codes(str)?;779	let mut out = String::new();780781	for code in codes {782		match code {783			Element::String(s) => {784				out.push_str(s);785			}786			Element::Code(c) => {787				// TODO: Operate on ref788				let f: IStr = c.mkey.into();789				let width = match c.width {790					Width::Star => {791						bail!(CannotUseStarWidthWithObject);792					}793					Width::Fixed(n) => n,794				};795				let precision = match c.precision {796					Some(Width::Star) => {797						bail!(CannotUseStarWidthWithObject);798					}799					Some(Width::Fixed(n)) => Some(n),800					None => None,801				};802803				let value = if c.convtype == ConvTypeV::Percent {804					Val::Null805				} else {806					if f.is_empty() {807						bail!(MappingKeysRequired);808					}809					if let Some(v) = values.get(f.clone())? {810						v811					} else {812						get_dotted_field(values.clone(), &f)?813					}814				};815816				format_code(&mut out, &value, &c, width, precision)?;817			}818		}819	}820821	Ok(out)822}823824#[cfg(test)]825pub mod test_format {826	use super::*;827	use crate::val::NumValue;828829	#[test]830	fn parse() {831		assert_eq!(832			parse_codes(833				"How much error budget is left looking at our %.3f%% availability gurantees?"834			)835			.unwrap()836			.len(),837			4838		);839	}840841	fn num(v: f64) -> Val {842		Val::Num(NumValue::new(v).expect("finite"))843	}844845	#[test]846	fn octals() {847		assert_eq!(format_arr("%#o", &[num(8.0)]).unwrap(), "010");848		assert_eq!(format_arr("%#4o", &[num(8.0)]).unwrap(), " 010");849		assert_eq!(format_arr("%4o", &[num(8.0)]).unwrap(), "  10");850		assert_eq!(format_arr("%04o", &[num(8.0)]).unwrap(), "0010");851		assert_eq!(format_arr("%+4o", &[num(8.0)]).unwrap(), " +10");852		assert_eq!(format_arr("%+04o", &[num(8.0)]).unwrap(), "+010");853		assert_eq!(format_arr("%-4o", &[num(8.0)]).unwrap(), "10  ");854		assert_eq!(format_arr("%+-4o", &[num(8.0)]).unwrap(), "+10 ");855		assert_eq!(format_arr("%+-04o", &[num(8.0)]).unwrap(), "+10 ");856	}857858	#[test]859	fn percent_doesnt_consumes_values() {860		assert_eq!(861			format_arr(862				"How much error budget is left looking at our %.3f%% availability gurantees?",863				&[num(4.0)]864			)865			.unwrap(),866			"How much error budget is left looking at our 4.000% availability gurantees?"867		);868	}869}
after · 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/trace/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/trace/mod.rs
+++ b/crates/jrsonnet-evaluator/src/trace/mod.rs
@@ -129,6 +129,7 @@
 			} else {
 				false
 			};
+			#[expect(clippy::cast_possible_truncation, reason = "code is limited by 4gb")]
 			let mut location = path
 				.map_source_locations(&[offset as u32])
 				.into_iter()
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -157,7 +157,9 @@
 	}
 }
 
+#[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 {
@@ -179,6 +181,7 @@
 								stringify!($ty)
 							)
 						}
+						#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation, reason = "checked by TYPE")]
 						Ok(n as Self)
 					}
 					_ => unreachable!(),
@@ -198,6 +201,7 @@
 macro_rules! impl_bounded_int {
 	($($name:ident = $ty:ty)*) => {$(
 		#[derive(Clone, Copy)]
+		#[allow(clippy::cast_possible_truncation, reason = "overflow is api misuse")]
 		pub struct $name<const MIN: $ty, const MAX: $ty>($ty);
 		impl<const MIN: $ty, const MAX: $ty> $name<MIN, MAX> {
 			pub const fn new(value: $ty) -> Option<$name<MIN, MAX>> {
@@ -219,6 +223,7 @@
 		}
 
 		impl<const MIN: $ty, const MAX: $ty> Typed for $name<MIN, MAX> {
+			#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, reason = "overflow is api misuse")]
 			const TYPE: &'static ComplexValType =
 				&ComplexValType::BoundedNumber(
 					Some(MIN as f64),
@@ -239,6 +244,7 @@
 								stringify!($ty)
 							)
 						}
+						#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "overflow is api misuse, the range is checked by TYPE")]
 						Ok(Self(n as $ty))
 					}
 					_ => unreachable!(),
@@ -318,6 +324,11 @@
 				if n.trunc() != n {
 					bail!("cannot convert number with fractional part to usize")
 				}
+				#[allow(
+					clippy::cast_possible_truncation,
+					clippy::cast_sign_loss,
+					reason = "the range is checked by TYPE"
+				)]
 				Ok(n as Self)
 			}
 			_ => unreachable!(),
modifiedcrates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -295,8 +295,10 @@
 				};
 				let mut get_idx = |pos: Option<i32>, default| {
 					match pos {
-						Some(v) if v < 0 => get_len().saturating_sub((-v) as usize),
+						#[expect(clippy::cast_sign_loss, reason = "abs value is used")]
+						Some(v) if v < 0 => get_len().saturating_sub((-v as isize) as usize),
 						// No need to clamp, as iterator interface is used
+						#[expect(clippy::cast_sign_loss, reason = "abs value is used")]
 						Some(v) => v as usize,
 						None => default,
 					}
@@ -322,6 +324,10 @@
 			Self::Arr(arr) => Ok(Self::Arr(arr.clone().slice(
 				index,
 				end,
+				#[expect(
+					clippy::cast_possible_truncation,
+					reason = "overflow will result with skip too large which would be equivalent"
+				)]
 				step.map(|v| NonZeroU32::new(v.value() as u32).expect("bounded != 0")),
 			))),
 		}
@@ -446,6 +452,7 @@
 		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)
 	}
 }
@@ -520,6 +527,7 @@
 			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)
modifiedcrates/jrsonnet-interner/src/inner.rsdiffbeforeafterboth
--- a/crates/jrsonnet-interner/src/inner.rs
+++ b/crates/jrsonnet-interner/src/inner.rs
@@ -67,7 +67,7 @@
 			.cast();
 			assert!(!data.is_null());
 			*data = InnerHeader::new(bytes.len().try_into().expect("bytes > 4GB"), is_utf8);
-			ptr::copy_nonoverlapping(bytes.as_ptr(), data.offset(1).cast::<u8>(), bytes.len());
+			ptr::copy_nonoverlapping(bytes.as_ptr(), data.add(1).cast::<u8>(), bytes.len());
 			Self(UnsafeCell::new(NonNull::new_unchecked(data)))
 		}
 	}
@@ -89,10 +89,7 @@
 		let size = unsafe { (*header).size };
 		// SAFETY: bytes after data is allocated to be exactly data.size in length
 		unsafe {
-			slice::from_raw_parts(
-				(*self.0.get()).as_ptr().offset(1).cast::<u8>(),
-				size as usize,
-			)
+			slice::from_raw_parts((*self.0.get()).as_ptr().add(1).cast::<u8>(), size as usize)
 		}
 	}
 
@@ -156,7 +153,7 @@
 	}
 	pub fn as_ptr(this: &Self) -> *const u8 {
 		// SAFETY: data is initialized
-		unsafe { (*this.0.get()).as_ptr().offset(1).cast() }
+		unsafe { (*this.0.get()).as_ptr().add(1).cast() }
 	}
 
 	pub fn strong_count(this: &Self) -> u32 {
modifiedcrates/jrsonnet-ir-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/lib.rs
+++ b/crates/jrsonnet-ir-parser/src/lib.rs
@@ -638,6 +638,7 @@
 	}
 }
 
+#[allow(clippy::too_many_lines)]
 fn expr_basic(p: &mut Parser<'_>) -> Result<Expr> {
 	if let Some(lit) = literal(p) {
 		return Ok(Expr::Literal(lit));
@@ -764,7 +765,6 @@
 		}
 
 		SyntaxKind::IDENT => {
-			let text = p.text();
 			let n = spanned(p, |p| {
 				let s: IStr = p.text().into();
 				p.eat_any();
@@ -1005,8 +1005,9 @@
 }
 
 pub fn string_to_expr(s: IStr, settings: &ParserSettings) -> Spanned<Expr> {
-	let len = s.len();
-	Spanned::new(Expr::Str(s), Span(settings.source.clone(), 0, len as u32))
+	let len = u32::try_from(s.len()).expect("code size is limited by 4gb");
+
+	Spanned::new(Expr::Str(s), Span(settings.source.clone(), 0, len))
 }
 
 #[cfg(test)]
modifiedcrates/jrsonnet-lexer/src/lex.rsdiffbeforeafterboth
--- a/crates/jrsonnet-lexer/src/lex.rs
+++ b/crates/jrsonnet-lexer/src/lex.rs
@@ -60,7 +60,10 @@
 			range: {
 				let Range { start, end } = self.inner.span();
 
-				Span(start as u32, end as u32)
+				Span(
+					u32::try_from(start).expect("code size is limited by 4gb"),
+					u32::try_from(end).expect("code size is limited by 4gb"),
+				)
 			},
 		})
 	}
modifiedcrates/jrsonnet-stdlib/src/arrays.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/arrays.rs
+++ b/crates/jrsonnet-stdlib/src/arrays.rs
@@ -17,7 +17,11 @@
 }
 
 #[builtin]
-pub fn builtin_make_array(sz: BoundedI32<0, { i32::MAX }>, func: FuncVal) -> Result<ArrValue> {
+pub fn builtin_make_array(
+	// Can't use usize because range_exclusive is over i32
+	sz: BoundedI32<0, { i32::MAX }>,
+	func: FuncVal,
+) -> Result<ArrValue> {
 	if *sz == 0 {
 		return Ok(ArrValue::empty());
 	}
@@ -25,6 +29,7 @@
 		// TODO: Different mapped array impl avoiding allocating unnecessary vals
 		|| Ok(ArrValue::range_exclusive(0, *sz).map(FromUntyped::from_untyped(Val::Func(func))?)),
 		|trivial| {
+			#[expect(clippy::cast_sign_loss, reason = "sz is bounded to be larger than 0")]
 			let mut out = Vec::with_capacity(*sz as usize);
 			for _ in 0..*sz {
 				out.push(trivial.clone());
@@ -363,6 +368,10 @@
 	if arr.is_empty() {
 		return eval_on_empty(onEmpty);
 	}
+	#[expect(
+		clippy::cast_precision_loss,
+		reason = "array sizes are bounded to i32 len"
+	)]
 	Ok(Val::try_num(arr.iter().sum::<f64>() / (arr.len() as f64))?)
 }
 
@@ -378,6 +387,11 @@
 pub fn builtin_remove(arr: ArrValue, elem: Val) -> Result<ArrValue> {
 	for (index, item) in arr.iter().enumerate() {
 		if equals(&item?, &elem)? {
+			#[expect(
+				clippy::cast_possible_truncation,
+				clippy::cast_possible_wrap,
+				reason = "array sizes are bounded to i32 len"
+			)]
 			return builtin_remove_at(arr.clone(), index as i32);
 		}
 	}
modifiedcrates/jrsonnet-stdlib/src/math.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/math.rs
+++ b/crates/jrsonnet-stdlib/src/math.rs
@@ -120,6 +120,7 @@
 		let lg = s.abs().log2();
 		let x = (lg - lg.floor() - 1.0).exp2();
 		let exp = lg.floor() + 1.0;
+		#[expect(clippy::cast_possible_truncation, reason = "exponent can fit in i16")]
 		(s.signum() * x, exp as i16)
 	}
 }
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -66,6 +66,7 @@
               "clippy"
               "rustc"
               "rust-src"
+              "rust-analyzer"
             ])
             rustfmt
           ];