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

difftreelog

refactor reenable clippy integer cast checks

slqpxoouYaroslav Bolyukin2026-04-25parent: #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
--- a/crates/jrsonnet-evaluator/src/stdlib/format.rs
+++ b/crates/jrsonnet-evaluator/src/stdlib/format.rs
@@ -1,5 +1,10 @@
 //! faster std.format impl
 #![allow(clippy::too_many_arguments)]
+#![expect(
+	clippy::cast_possible_truncation,
+	clippy::cast_sign_loss,
+	reason = "many safe integer casts, behavior on overflow is not specified"
+)]
 
 use jrsonnet_gcmodule::Trace;
 use jrsonnet_interner::IStr;
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
before · crates/jrsonnet-stdlib/src/arrays.rs
1#![allow(non_snake_case)]23use jrsonnet_evaluator::{4	Either, IStr, ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val, bail,5	function::{FuncVal, NativeFn, builtin},6	runtime_error,7	typed::{BoundedI32, BoundedUsize, Either2, FromUntyped},8	val::{ArrValue, IndexableVal, equals},9};1011pub fn eval_on_empty(on_empty: Option<Thunk<Val>>) -> Result<Val> {12	if let Some(on_empty) = on_empty {13		on_empty.evaluate()14	} else {15		bail!("expected non-empty array")16	}17}1819#[builtin]20pub fn builtin_make_array(sz: BoundedI32<0, { i32::MAX }>, func: FuncVal) -> Result<ArrValue> {21	if *sz == 0 {22		return Ok(ArrValue::empty());23	}24	func.evaluate_trivial().map_or_else(25		// TODO: Different mapped array impl avoiding allocating unnecessary vals26		|| Ok(ArrValue::range_exclusive(0, *sz).map(FromUntyped::from_untyped(Val::Func(func))?)),27		|trivial| {28			let mut out = Vec::with_capacity(*sz as usize);29			for _ in 0..*sz {30				out.push(trivial.clone());31			}32			Ok(ArrValue::eager(out))33		},34	)35}3637#[builtin]38pub fn builtin_repeat(what: Either![IStr, ArrValue], count: usize) -> Result<Val> {39	Ok(match what {40		Either2::A(s) => Val::string(s.repeat(count)),41		Either2::B(arr) => Val::Arr(42			ArrValue::repeated(arr, count)43				.ok_or_else(|| runtime_error!("repeated length overflow"))?,44		),45	})46}4748#[builtin]49pub fn builtin_slice(50	indexable: IndexableVal,51	index: Option<Option<i32>>,52	end: Option<Option<i32>>,53	step: Option<Option<BoundedUsize<1, { i32::MAX as usize }>>>,54) -> Result<Val> {55	indexable56		.slice(index.flatten(), end.flatten(), step.flatten())57		.map(Val::from)58}5960#[builtin]61pub fn builtin_map(func: NativeFn!((Val) -> Val), arr: IndexableVal) -> ArrValue {62	let arr = arr.to_array();63	arr.map(func)64}6566#[builtin]67pub fn builtin_map_with_index(func: NativeFn!((u32, Val) -> Val), arr: IndexableVal) -> ArrValue {68	let arr = arr.to_array();69	arr.map_with_index(func)70}7172#[builtin]73pub fn builtin_map_with_key(74	func: NativeFn!((IStr, Val) -> Val),75	obj: ObjValue,76) -> Result<ObjValue> {77	let mut out = ObjValueBuilder::new();78	for (k, v) in obj.iter(79		// Makes sense mapped object should be ordered the same way, should not break anything when the output is not ordered (the default).80		// The thrown error might be different, but jsonnet81		// does not specify the evaluation order.82		#[cfg(feature = "exp-preserve-order")]83		true,84	) {85		let v = v?;86		out.field(k.clone()).value(func.call(k, v)?);87	}88	Ok(out.build())89}9091#[builtin]92pub fn builtin_flatmap(93	func: NativeFn!((Either![String, Val]) -> Val),94	arr: IndexableVal,95) -> Result<IndexableVal> {96	use std::fmt::Write;97	match arr {98		IndexableVal::Str(str) => {99			let mut out = String::new();100			for c in str.chars() {101				match func.call(Either2::A(c.to_string()))? {102					Val::Str(o) => write!(out, "{o}").unwrap(),103					Val::Null => {}104					_ => bail!("in std.join all items should be strings"),105				}106			}107			Ok(IndexableVal::Str(out.into()))108		}109		IndexableVal::Arr(a) => {110			let mut out = Vec::new();111			for el in a.iter() {112				let el = el?;113				match func.call(Either2::B(el))? {114					Val::Arr(o) => {115						for oe in o.iter() {116							out.push(oe?);117						}118					}119					Val::Null => {}120					_ => bail!("in std.join all items should be arrays"),121				}122			}123			Ok(IndexableVal::Arr(out.into()))124		}125	}126}127128type FilterFunc = NativeFn!((Thunk<Val>) -> bool);129130#[builtin]131pub fn builtin_filter(func: FilterFunc, arr: ArrValue) -> Result<ArrValue> {132	arr.filter(func)133}134135#[builtin]136pub fn builtin_filter_map(137	filter_func: FilterFunc,138	map_func: NativeFn!((Val) -> Val),139	arr: ArrValue,140) -> Result<ArrValue> {141	Ok(arr.filter(filter_func)?.map(map_func))142}143144#[builtin]145pub fn builtin_foldl(146	func: NativeFn!((Val, Either![Val, char]) -> Val),147	arr: Either![ArrValue, IStr],148	init: Val,149) -> Result<Val> {150	let mut acc = init;151	match arr {152		Either2::A(arr) => {153			for i in arr.iter() {154				acc = func.call(acc, Either2::A(i?))?;155			}156		}157		Either2::B(arr) => {158			for c in arr.chars() {159				acc = func.call(acc, Either2::B(c))?;160			}161		}162	}163	Ok(acc)164}165166#[builtin]167pub fn builtin_foldr(168	func: NativeFn!((Either![Val, char], Val) -> Val),169	arr: Either![ArrValue, IStr],170	init: Val,171) -> Result<Val> {172	let mut acc = init;173	match arr {174		Either2::A(arr) => {175			for i in arr.iter().rev() {176				acc = func.call(Either2::A(i?), acc)?;177			}178		}179		Either2::B(arr) => {180			for c in arr.chars().rev() {181				acc = func.call(Either2::B(c), acc)?;182			}183		}184	}185	Ok(acc)186}187188#[builtin]189pub fn builtin_range(from: i32, to: i32) -> Result<ArrValue> {190	if to < from {191		return Ok(ArrValue::empty());192	}193	Ok(ArrValue::range_inclusive(from, to))194}195196#[builtin]197pub fn builtin_join(sep: IndexableVal, arr: ArrValue) -> Result<IndexableVal> {198	use std::fmt::Write;199	Ok(match sep {200		IndexableVal::Arr(joiner_items) => {201			let mut out = Vec::new();202203			let mut first = true;204			for item in arr.iter() {205				let item = item?.clone();206				if let Val::Arr(items) = item {207					if !first {208						out.reserve(joiner_items.len());209						// TODO: extend210						for item in joiner_items.iter() {211							out.push(item?);212						}213					}214					first = false;215					out.reserve(items.len());216					for item in items.iter() {217						out.push(item?);218					}219				} else if matches!(item, Val::Null) {220				} else {221					bail!("in std.join all items should be arrays");222				}223			}224225			IndexableVal::Arr(out.into())226		}227		IndexableVal::Str(sep) => {228			let mut out = String::new();229230			let mut first = true;231			for item in arr.iter() {232				let item = item?.clone();233				if let Val::Str(item) = item {234					if !first {235						out += &sep;236					}237					first = false;238					write!(out, "{item}").unwrap();239				} else if matches!(item, Val::Null) {240				} else {241					bail!("in std.join all items should be strings");242				}243			}244245			IndexableVal::Str(out.into())246		}247	})248}249250#[builtin]251pub fn builtin_lines(arr: ArrValue) -> Result<IndexableVal> {252	builtin_join(253		IndexableVal::Str("\n".into()),254		ArrValue::extended(arr, ArrValue::eager(vec![Val::string("")])),255	)256}257258#[builtin]259pub fn builtin_resolve_path(f: String, r: String) -> String {260	let Some(pos) = f.rfind('/') else {261		return r;262	};263	format!("{}{}", &f[..=pos], r)264}265266pub fn deep_join_inner(out: &mut String, arr: IndexableVal) -> Result<()> {267	use std::fmt::Write;268	match arr {269		IndexableVal::Str(s) => write!(out, "{s}").expect("no error"),270		IndexableVal::Arr(arr) => {271			for ele in arr.iter() {272				let indexable = IndexableVal::from_untyped(ele?)?;273				deep_join_inner(out, indexable)?;274			}275		}276	}277	Ok(())278}279280#[builtin]281pub fn builtin_deep_join(arr: IndexableVal) -> Result<String> {282	let mut out = String::new();283	deep_join_inner(&mut out, arr)?;284	Ok(out)285}286287#[builtin]288pub fn builtin_reverse(arr: ArrValue) -> ArrValue {289	arr.reversed()290}291292#[builtin]293pub fn builtin_any(arr: ArrValue) -> Result<bool> {294	for v in arr.iter() {295		let v = bool::from_untyped(v?)?;296		if v {297			return Ok(true);298		}299	}300	Ok(false)301}302303#[builtin]304pub fn builtin_all(arr: ArrValue) -> Result<bool> {305	for v in arr.iter() {306		let v = bool::from_untyped(v?)?;307		if !v {308			return Ok(false);309		}310	}311	Ok(true)312}313314#[builtin]315pub fn builtin_member(arr: IndexableVal, x: Val) -> Result<bool> {316	match arr {317		IndexableVal::Str(str) => {318			let x: IStr = IStr::from_untyped(x)?;319			Ok(!x.is_empty() && str.contains(&*x))320		}321		IndexableVal::Arr(a) => {322			for item in a.iter() {323				let item = item?;324				if equals(&item, &x)? {325					return Ok(true);326				}327			}328			Ok(false)329		}330	}331}332333#[builtin]334pub fn builtin_find(value: Val, arr: ArrValue) -> Result<Vec<usize>> {335	let mut out = Vec::new();336	for (i, ele) in arr.iter().enumerate() {337		let ele = ele?;338		if equals(&ele, &value)? {339			out.push(i);340		}341	}342	Ok(out)343}344345#[builtin]346pub fn builtin_contains(arr: IndexableVal, elem: Val) -> Result<bool> {347	builtin_member(arr, elem)348}349350#[builtin]351pub fn builtin_count(arr: ArrValue, x: Val) -> Result<usize> {352	let mut count = 0;353	for item in arr.iter() {354		if equals(&item?, &x)? {355			count += 1;356		}357	}358	Ok(count)359}360361#[builtin]362pub fn builtin_avg(arr: Vec<f64>, onEmpty: Option<Thunk<Val>>) -> Result<Val> {363	if arr.is_empty() {364		return eval_on_empty(onEmpty);365	}366	Ok(Val::try_num(arr.iter().sum::<f64>() / (arr.len() as f64))?)367}368369#[builtin]370pub fn builtin_remove_at(arr: ArrValue, at: i32) -> Result<ArrValue> {371	let newArrLeft = arr.clone().slice(None, Some(at), None);372	let newArrRight = arr.slice(Some(at + 1), None, None);373374	Ok(ArrValue::extended(newArrLeft, newArrRight))375}376377#[builtin]378pub fn builtin_remove(arr: ArrValue, elem: Val) -> Result<ArrValue> {379	for (index, item) in arr.iter().enumerate() {380		if equals(&item?, &elem)? {381			return builtin_remove_at(arr.clone(), index as i32);382		}383	}384	Ok(arr)385}386387#[builtin]388pub fn builtin_flatten_arrays(arrs: Vec<ArrValue>) -> ArrValue {389	pub fn flatten_inner(values: &[ArrValue]) -> ArrValue {390		if values.len() == 1 {391			return values[0].clone();392		} else if values.len() == 2 {393			return ArrValue::extended(values[0].clone(), values[1].clone());394		}395		let (a, b) = values.split_at(values.len() / 2);396		ArrValue::extended(flatten_inner(a), flatten_inner(b))397	}398	if arrs.is_empty() {399		return ArrValue::empty();400	} else if arrs.len() == 1 {401		return arrs.into_iter().next().expect("single");402	}403	flatten_inner(&arrs)404}405406#[builtin]407pub fn builtin_flatten_deep_array(value: Val) -> Result<Vec<Val>> {408	fn process(value: Val, out: &mut Vec<Val>) -> Result<()> {409		match value {410			Val::Arr(arr) => {411				for ele in arr.iter() {412					process(ele?, out)?;413				}414			}415			_ => out.push(value),416		}417		Ok(())418	}419	let mut out = Vec::new();420	process(value, &mut out)?;421	Ok(out)422}423424#[builtin]425pub fn builtin_prune(426	a: Val,427428	#[default(false)]429	#[cfg(feature = "exp-preserve-order")]430	preserve_order: bool,431) -> Result<Val> {432	fn is_content(val: &Val) -> bool {433		match val {434			Val::Null => false,435			Val::Arr(a) => !a.is_empty(),436			Val::Obj(o) => !o.is_empty(),437			_ => true,438		}439	}440	Ok(match a {441		Val::Arr(a) => {442			let mut out = Vec::new();443			for (i, ele) in a.iter().enumerate() {444				let ele = ele445					.and_then(|v| {446						builtin_prune(447							v,448							#[cfg(feature = "exp-preserve-order")]449							preserve_order,450						)451					})452					.with_description(|| format!("elem <{i}> pruning"))?;453				if is_content(&ele) {454					out.push(ele);455				}456			}457			Val::Arr(ArrValue::eager(out))458		}459		Val::Obj(o) => {460			let mut out = ObjValueBuilder::new();461			for (name, value) in o.iter(462				#[cfg(feature = "exp-preserve-order")]463				preserve_order,464			) {465				let value = value466					.and_then(|v| {467						builtin_prune(468							v,469							#[cfg(feature = "exp-preserve-order")]470							preserve_order,471						)472					})473					.with_description(|| format!("field <{name}> pruning"))?;474				if !is_content(&value) {475					continue;476				}477				out.field(name).value(value);478			}479			Val::Obj(out.build())480		}481		_ => a,482	})483}
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
           ];