git.delta.rocks / jrsonnet / refs/commits / b5d51b90d7ba

difftreelog

feat `*stripChars` builtins

Petr Portnov2024-06-18parent: #eced1a8.patch.diff
in: master

12 files changed

added.editorconfigdiffbeforeafterboth
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[tests/golden/*.jsonnet.golden]
+generated_code = true
+indent_style = space
+indent_size = 4
+insert_final_newline = false
modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -90,9 +90,9 @@
 
 [[package]]
 name = "anyhow"
-version = "1.0.83"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
 
 [[package]]
 name = "autocfg"
@@ -102,9 +102,9 @@
 
 [[package]]
 name = "base64"
-version = "0.21.7"
+version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
 [[package]]
 name = "beef"
@@ -194,7 +194,7 @@
  "heck",
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.64",
 ]
 
 [[package]]
@@ -258,6 +258,12 @@
 ]
 
 [[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
 name = "digest"
 version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -289,9 +295,9 @@
 
 [[package]]
 name = "either"
-version = "1.11.0"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
+checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
 
 [[package]]
 name = "encode_unicode"
@@ -412,9 +418,9 @@
 
 [[package]]
 name = "insta"
-version = "1.38.0"
+version = "1.39.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc"
+checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5"
 dependencies = [
  "console",
  "lazy_static",
@@ -430,9 +436,9 @@
 
 [[package]]
 name = "itertools"
-version = "0.12.1"
+version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
 dependencies = [
  "either",
 ]
@@ -547,7 +553,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.64",
 ]
 
 [[package]]
@@ -608,6 +614,17 @@
 ]
 
 [[package]]
+name = "json-structural-diff"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25c7940d3c84d2079306c176c7b2b37622b6bc5e43fbd1541b1e4a4e1fd02045"
+dependencies = [
+ "difflib",
+ "regex",
+ "serde_json",
+]
+
+[[package]]
 name = "keccak"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -624,9 +641,9 @@
 
 [[package]]
 name = "libc"
-version = "0.2.154"
+version = "0.2.155"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
 
 [[package]]
 name = "libjsonnet"
@@ -646,9 +663,9 @@
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.13"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
 [[package]]
 name = "lock_api"
@@ -681,7 +698,7 @@
  "proc-macro2",
  "quote",
  "regex-syntax",
- "syn 2.0.61",
+ "syn 2.0.64",
 ]
 
 [[package]]
@@ -989,22 +1006,22 @@
 
 [[package]]
 name = "serde"
-version = "1.0.201"
+version = "1.0.202"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
+checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.201"
+version = "1.0.202"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
+checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.64",
 ]
 
 [[package]]
@@ -1121,9 +1138,9 @@
 
 [[package]]
 name = "syn"
-version = "2.0.61"
+version = "2.0.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
+checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1149,7 +1166,9 @@
  "jrsonnet-evaluator",
  "jrsonnet-gcmodule",
  "jrsonnet-stdlib",
+ "json-structural-diff",
  "serde",
+ "serde_json",
 ]
 
 [[package]]
@@ -1160,22 +1179,22 @@
 
 [[package]]
 name = "thiserror"
-version = "1.0.60"
+version = "1.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.60"
+version = "1.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.64",
 ]
 
 [[package]]
@@ -1347,5 +1366,5 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.64",
 ]
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,8 +44,8 @@
 serde_yaml_with_quirks = "0.8.24"
 
 # Error handling
-anyhow = "1.0.80"
-thiserror = "1.0"
+anyhow = "1.0.83"
+thiserror = "1.0.60"
 
 # Code formatting
 dprint-core = "0.65.0"
@@ -63,20 +63,20 @@
 # Source code parsing.
 # Jrsonnet has two parsers for jsonnet - one is for execution, and another is for better parsing diagnostics/lints/LSP.
 # First (and fast one) is based on peg, second is based on rowan.
-peg = "0.8.2"
+peg = "0.8.3"
 logos = "0.14.0"
 ungrammar = "1.16.1"
-rowan = "0.15"
+rowan = "0.15.15"
 
 mimallocator = "0.1.3"
 indoc = "2.0"
-insta = "1.35"
+insta = "1.39"
 tempfile = "3.10"
 pathdiff = "0.2.1"
-hashbrown = "0.14.3"
+hashbrown = "0.14.5"
 static_assertions = "1.1"
 rustc-hash = "1.1"
-num-bigint = "0.4.4"
+num-bigint = "0.4.5"
 derivative = "2.2.0"
 strsim = "0.11.0"
 structdump = "0.2.0"
@@ -84,16 +84,18 @@
 quote = "1.0"
 syn = "2.0"
 drop_bomb = "0.1.5"
-base64 = "0.21.7"
+base64 = "0.22.1"
 indexmap = "2.2.3"
-itertools = "0.12.1"
-xshell = "0.2.5"
+itertools = "0.13.0"
+xshell = "0.2.6"
 
 lsp-server = "0.7.6"
-lsp-types = "0.95.0"
+lsp-types = "0.96.0"
 
-regex = "1.10.3"
-lru = "0.12.2"
+regex = "1.10"
+lru = "0.12.3"
+
+json-structural-diff = "0.1.0"
 
 [workspace.lints.rust]
 unsafe_op_in_unsafe_fn = "deny"
modifiedcrates/jrsonnet-evaluator/src/arr/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/mod.rs
+++ b/crates/jrsonnet-evaluator/src/arr/mod.rs
@@ -11,7 +11,7 @@
 
 /// Represents a Jsonnet array value.
 #[derive(Debug, Clone, Trace)]
-// may contrain other ArrValue
+// may contain other ArrValue
 #[trace(tracking(force))]
 pub struct ArrValue(Cc<TraceBox<dyn ArrayLike>>);
 
modifiedcrates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/val.rs
1use std::{2	cell::RefCell,3	fmt::{self, Debug, Display},4	mem::replace,5	num::NonZeroU32,6	rc::Rc,7};89use jrsonnet_gcmodule::{Cc, Trace};10use jrsonnet_interner::IStr;11use jrsonnet_types::ValType;1213pub use crate::arr::{ArrValue, ArrayLike};14use crate::{15	bail,16	error::{Error, ErrorKind::*},17	function::FuncVal,18	gc::{GcHashMap, TraceBox},19	manifest::{ManifestFormat, ToStringFormat},20	tb,21	typed::BoundedUsize,22	ObjValue, Result, Unbound, WeakObjValue,23};2425pub trait ThunkValue: Trace {26	type Output;27	fn get(self: Box<Self>) -> Result<Self::Output>;28}2930#[derive(Trace)]31enum ThunkInner<T: Trace> {32	Computed(T),33	Errored(Error),34	Waiting(TraceBox<dyn ThunkValue<Output = T>>),35	Pending,36}3738/// Lazily evaluated value39#[allow(clippy::module_name_repetitions)]40#[derive(Clone, Trace)]41pub struct Thunk<T: Trace>(Cc<RefCell<ThunkInner<T>>>);4243impl<T: Trace> Thunk<T> {44	pub fn evaluated(val: T) -> Self {45		Self(Cc::new(RefCell::new(ThunkInner::Computed(val))))46	}47	pub fn new(f: impl ThunkValue<Output = T> + 'static) -> Self {48		Self(Cc::new(RefCell::new(ThunkInner::Waiting(tb!(f)))))49	}50	pub fn errored(e: Error) -> Self {51		Self(Cc::new(RefCell::new(ThunkInner::Errored(e))))52	}53	pub fn result(res: Result<T, Error>) -> Self {54		match res {55			Ok(o) => Self::evaluated(o),56			Err(e) => Self::errored(e),57		}58	}59}6061impl<T> Thunk<T>62where63	T: Clone + Trace,64{65	pub fn force(&self) -> Result<()> {66		self.evaluate()?;67		Ok(())68	}6970	/// Evaluate thunk, or return cached value71	///72	/// # Errors73	///74	/// - Lazy value evaluation returned error75	/// - This method was called during inner value evaluation76	pub fn evaluate(&self) -> Result<T> {77		match &*self.0.borrow() {78			ThunkInner::Computed(v) => return Ok(v.clone()),79			ThunkInner::Errored(e) => return Err(e.clone()),80			ThunkInner::Pending => return Err(InfiniteRecursionDetected.into()),81			ThunkInner::Waiting(..) => (),82		};83		let ThunkInner::Waiting(value) = replace(&mut *self.0.borrow_mut(), ThunkInner::Pending)84		else {85			unreachable!();86		};87		let new_value = match value.0.get() {88			Ok(v) => v,89			Err(e) => {90				*self.0.borrow_mut() = ThunkInner::Errored(e.clone());91				return Err(e);92			}93		};94		*self.0.borrow_mut() = ThunkInner::Computed(new_value.clone());95		Ok(new_value)96	}97}9899pub trait ThunkMapper<Input>: Trace {100	type Output;101	fn map(self, from: Input) -> Result<Self::Output>;102}103impl<Input> Thunk<Input>104where105	Input: Trace + Clone,106{107	pub fn map<M>(self, mapper: M) -> Thunk<M::Output>108	where109		M: ThunkMapper<Input>,110		M::Output: Trace,111	{112		#[derive(Trace)]113		struct Mapped<Input: Trace, Mapper: Trace> {114			inner: Thunk<Input>,115			mapper: Mapper,116		}117		impl<Input, Mapper> ThunkValue for Mapped<Input, Mapper>118		where119			Input: Trace + Clone,120			Mapper: ThunkMapper<Input>,121		{122			type Output = Mapper::Output;123124			fn get(self: Box<Self>) -> Result<Self::Output> {125				let value = self.inner.evaluate()?;126				let mapped = self.mapper.map(value)?;127				Ok(mapped)128			}129		}130131		Thunk::new(Mapped::<Input, M> {132			inner: self,133			mapper,134		})135	}136}137138impl<T: Trace> From<Result<T>> for Thunk<T> {139	fn from(value: Result<T>) -> Self {140		match value {141			Ok(o) => Self::evaluated(o),142			Err(e) => Self::errored(e),143		}144	}145}146impl<T, V: Trace> From<T> for Thunk<V>147where148	T: ThunkValue<Output = V>,149{150	fn from(value: T) -> Self {151		Self::new(value)152	}153}154155impl<T: Trace + Default> Default for Thunk<T> {156	fn default() -> Self {157		Self::evaluated(T::default())158	}159}160161type CacheKey = (Option<WeakObjValue>, Option<WeakObjValue>);162163#[derive(Trace, Clone)]164pub struct CachedUnbound<I, T>165where166	I: Unbound<Bound = T>,167	T: Trace,168{169	cache: Cc<RefCell<GcHashMap<CacheKey, T>>>,170	value: I,171}172impl<I: Unbound<Bound = T>, T: Trace> CachedUnbound<I, T> {173	pub fn new(value: I) -> Self {174		Self {175			cache: Cc::new(RefCell::new(GcHashMap::new())),176			value,177		}178	}179}180impl<I: Unbound<Bound = T>, T: Clone + Trace> Unbound for CachedUnbound<I, T> {181	type Bound = T;182	fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<T> {183		let cache_key = (184			sup.as_ref().map(|s| s.clone().downgrade()),185			this.as_ref().map(|t| t.clone().downgrade()),186		);187		{188			if let Some(t) = self.cache.borrow().get(&cache_key) {189				return Ok(t.clone());190			}191		}192		let bound = self.value.bind(sup, this)?;193194		{195			let mut cache = self.cache.borrow_mut();196			cache.insert(cache_key, bound.clone());197		}198199		Ok(bound)200	}201}202203impl<T: Debug + Trace> Debug for Thunk<T> {204	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {205		write!(f, "Lazy")206	}207}208impl<T: Trace> PartialEq for Thunk<T> {209	fn eq(&self, other: &Self) -> bool {210		Cc::ptr_eq(&self.0, &other.0)211	}212}213214/// Represents a Jsonnet value, which can be sliced or indexed (string or array).215#[allow(clippy::module_name_repetitions)]216pub enum IndexableVal {217	/// String.218	Str(IStr),219	/// Array.220	Arr(ArrValue),221}222impl IndexableVal {223	pub fn to_array(self) -> ArrValue {224		match self {225			Self::Str(s) => ArrValue::chars(s.chars()),226			Self::Arr(arr) => arr,227		}228	}229	/// Slice the value.230	///231	/// # Implementation232	///233	/// For strings, will create a copy of specified interval.234	///235	/// For arrays, nothing will be copied on this call, instead [`ArrValue::Slice`] view will be returned.236	pub fn slice(237		self,238		index: Option<i32>,239		end: Option<i32>,240		step: Option<BoundedUsize<1, { i32::MAX as usize }>>,241	) -> Result<Self> {242		match &self {243			Self::Str(s) => {244				let mut computed_len = None;245				let mut get_len = || {246					computed_len.map_or_else(247						|| {248							let len = s.chars().count();249							let _ = computed_len.insert(len);250							len251						},252						|len| len,253					)254				};255				let mut get_idx = |pos: Option<i32>, default| {256					match pos {257						Some(v) if v < 0 => get_len().saturating_sub((-v) as usize),258						// No need to clamp, as iterator interface is used259						Some(v) => v as usize,260						None => default,261					}262				};263264				let index = get_idx(index, 0);265				let end = get_idx(end, usize::MAX);266				let step = step.as_deref().copied().unwrap_or(1);267268				if index >= end {269					return Ok(Self::Str("".into()));270				}271272				Ok(Self::Str(273					(s.chars()274						.skip(index)275						.take(end - index)276						.step_by(step)277						.collect::<String>())278					.into(),279				))280			}281			Self::Arr(arr) => Ok(Self::Arr(arr.clone().slice(282				index,283				end,284				step.map(|v| NonZeroU32::new(v.value() as u32).expect("bounded != 0")),285			))),286		}287	}288}289290#[derive(Debug, Clone, Trace)]291pub enum StrValue {292	Flat(IStr),293	Tree(Rc<(StrValue, StrValue, usize)>),294}295impl StrValue {296	pub fn concat(a: Self, b: Self) -> Self {297		// TODO: benchmark for an optimal value, currently just a arbitrary choice298		const STRING_EXTEND_THRESHOLD: usize = 100;299300		if a.is_empty() {301			b302		} else if b.is_empty() {303			a304		} else if a.len() + b.len() < STRING_EXTEND_THRESHOLD {305			Self::Flat(format!("{a}{b}").into())306		} else {307			let len = a.len() + b.len();308			Self::Tree(Rc::new((a, b, len)))309		}310	}311	pub fn into_flat(self) -> IStr {312		#[cold]313		fn write_buf(s: &StrValue, out: &mut String) {314			match s {315				StrValue::Flat(f) => out.push_str(f),316				StrValue::Tree(t) => {317					write_buf(&t.0, out);318					write_buf(&t.1, out);319				}320			}321		}322		match self {323			Self::Flat(f) => f,324			Self::Tree(_) => {325				let mut buf = String::with_capacity(self.len());326				write_buf(&self, &mut buf);327				buf.into()328			}329		}330	}331	pub fn len(&self) -> usize {332		match self {333			Self::Flat(v) => v.len(),334			Self::Tree(t) => t.2,335		}336	}337	pub fn is_empty(&self) -> bool {338		match self {339			Self::Flat(v) => v.is_empty(),340			// Can't create non-flat empty string341			Self::Tree(_) => false,342		}343	}344}345impl<T> From<T> for StrValue346where347	IStr: From<T>,348{349	fn from(value: T) -> Self {350		Self::Flat(IStr::from(value))351	}352}353impl Display for StrValue {354	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {355		match self {356			Self::Flat(v) => write!(f, "{v}"),357			Self::Tree(t) => {358				write!(f, "{}", t.0)?;359				write!(f, "{}", t.1)360			}361		}362	}363}364impl PartialEq for StrValue {365	// False positive, into_flat returns not StrValue, but IStr, thus no infinite recursion here.366	#[allow(clippy::unconditional_recursion)]367	fn eq(&self, other: &Self) -> bool {368		let a = self.clone().into_flat();369		let b = other.clone().into_flat();370		a == b371	}372}373impl Eq for StrValue {}374impl PartialOrd for StrValue {375	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {376		Some(self.cmp(other))377	}378}379impl Ord for StrValue {380	fn cmp(&self, other: &Self) -> std::cmp::Ordering {381		let a = self.clone().into_flat();382		let b = other.clone().into_flat();383		a.cmp(&b)384	}385}386387/// Represents any valid Jsonnet value.388#[derive(Debug, Clone, Trace, Default)]389pub enum Val {390	/// Represents a Jsonnet boolean.391	Bool(bool),392	/// Represents a Jsonnet null value.393	#[default]394	Null,395	/// Represents a Jsonnet string.396	Str(StrValue),397	/// Represents a Jsonnet number.398	/// Should be finite, and not NaN399	/// This restriction isn't enforced by enum, as enum field can't be marked as private400	Num(f64),401	/// Experimental bigint402	#[cfg(feature = "exp-bigint")]403	BigInt(#[trace(skip)] Box<num_bigint::BigInt>),404	/// Represents a Jsonnet array.405	Arr(ArrValue),406	/// Represents a Jsonnet object.407	Obj(ObjValue),408	/// Represents a Jsonnet function.409	Func(FuncVal),410}411412#[cfg(target_pointer_width = "64")]413static_assertions::assert_eq_size!(Val, [u8; 24]);414415impl From<IndexableVal> for Val {416	fn from(v: IndexableVal) -> Self {417		match v {418			IndexableVal::Str(s) => Self::string(s),419			IndexableVal::Arr(a) => Self::Arr(a),420		}421	}422}423424impl Val {425	pub const fn as_bool(&self) -> Option<bool> {426		match self {427			Self::Bool(v) => Some(*v),428			_ => None,429		}430	}431	pub const fn as_null(&self) -> Option<()> {432		match self {433			Self::Null => Some(()),434			_ => None,435		}436	}437	pub fn as_str(&self) -> Option<IStr> {438		match self {439			Self::Str(s) => Some(s.clone().into_flat()),440			_ => None,441		}442	}443	pub const fn as_num(&self) -> Option<f64> {444		match self {445			Self::Num(n) => Some(*n),446			_ => None,447		}448	}449	pub fn as_arr(&self) -> Option<ArrValue> {450		match self {451			Self::Arr(a) => Some(a.clone()),452			_ => None,453		}454	}455	pub fn as_obj(&self) -> Option<ObjValue> {456		match self {457			Self::Obj(o) => Some(o.clone()),458			_ => None,459		}460	}461	pub fn as_func(&self) -> Option<FuncVal> {462		match self {463			Self::Func(f) => Some(f.clone()),464			_ => None,465		}466	}467468	/// Creates `Val::Num` after checking for numeric overflow.469	/// As numbers are `f64`, we can just check for their finity.470	pub fn new_checked_num(num: f64) -> Result<Self> {471		if num.is_finite() {472			Ok(Self::Num(num))473		} else {474			bail!("overflow")475		}476	}477478	pub const fn value_type(&self) -> ValType {479		match self {480			Self::Str(..) => ValType::Str,481			Self::Num(..) => ValType::Num,482			#[cfg(feature = "exp-bigint")]483			Self::BigInt(..) => ValType::BigInt,484			Self::Arr(..) => ValType::Arr,485			Self::Obj(..) => ValType::Obj,486			Self::Bool(_) => ValType::Bool,487			Self::Null => ValType::Null,488			Self::Func(..) => ValType::Func,489		}490	}491492	pub fn manifest(&self, format: impl ManifestFormat) -> Result<String> {493		fn manifest_dyn(val: &Val, manifest: &dyn ManifestFormat) -> Result<String> {494			manifest.manifest(val.clone())495		}496		manifest_dyn(self, &format)497	}498499	pub fn to_string(&self) -> Result<IStr> {500		Ok(match self {501			Self::Bool(true) => "true".into(),502			Self::Bool(false) => "false".into(),503			Self::Null => "null".into(),504			Self::Str(s) => s.clone().into_flat(),505			_ => self.manifest(ToStringFormat).map(IStr::from)?,506		})507	}508509	pub fn into_indexable(self) -> Result<IndexableVal> {510		Ok(match self {511			Self::Str(s) => IndexableVal::Str(s.into_flat()),512			Self::Arr(arr) => IndexableVal::Arr(arr),513			_ => bail!(ValueIsNotIndexable(self.value_type())),514		})515	}516517	pub fn function(function: impl Into<FuncVal>) -> Self {518		Self::Func(function.into())519	}520	pub fn string(string: impl Into<StrValue>) -> Self {521		Self::Str(string.into())522	}523}524525impl From<IStr> for Val {526	fn from(value: IStr) -> Self {527		Self::string(value)528	}529}530impl From<String> for Val {531	fn from(value: String) -> Self {532		Self::string(value)533	}534}535impl From<&str> for Val {536	fn from(value: &str) -> Self {537		Self::string(value)538	}539}540impl From<ObjValue> for Val {541	fn from(value: ObjValue) -> Self {542		Self::Obj(value)543	}544}545546const fn is_function_like(val: &Val) -> bool {547	matches!(val, Val::Func(_))548}549550/// Native implementation of `std.primitiveEquals`551pub fn primitive_equals(val_a: &Val, val_b: &Val) -> Result<bool> {552	Ok(match (val_a, val_b) {553		(Val::Bool(a), Val::Bool(b)) => a == b,554		(Val::Null, Val::Null) => true,555		(Val::Str(a), Val::Str(b)) => a == b,556		(Val::Num(a), Val::Num(b)) => (a - b).abs() <= f64::EPSILON,557		#[cfg(feature = "exp-bigint")]558		(Val::BigInt(a), Val::BigInt(b)) => a == b,559		(Val::Arr(_), Val::Arr(_)) => {560			bail!("primitiveEquals operates on primitive types, got array")561		}562		(Val::Obj(_), Val::Obj(_)) => {563			bail!("primitiveEquals operates on primitive types, got object")564		}565		(a, b) if is_function_like(a) && is_function_like(b) => {566			bail!("cannot test equality of functions")567		}568		(_, _) => false,569	})570}571572/// Native implementation of `std.equals`573pub fn equals(val_a: &Val, val_b: &Val) -> Result<bool> {574	if val_a.value_type() != val_b.value_type() {575		return Ok(false);576	}577	match (val_a, val_b) {578		(Val::Arr(a), Val::Arr(b)) => {579			if ArrValue::ptr_eq(a, b) {580				return Ok(true);581			}582			if a.len() != b.len() {583				return Ok(false);584			}585			for (a, b) in a.iter().zip(b.iter()) {586				if !equals(&a?, &b?)? {587					return Ok(false);588				}589			}590			Ok(true)591		}592		(Val::Obj(a), Val::Obj(b)) => {593			if ObjValue::ptr_eq(a, b) {594				return Ok(true);595			}596			let fields = a.fields(597				#[cfg(feature = "exp-preserve-order")]598				false,599			);600			if fields601				!= b.fields(602					#[cfg(feature = "exp-preserve-order")]603					false,604				) {605				return Ok(false);606			}607			for field in fields {608				if !equals(609					&a.get(field.clone())?.expect("field exists"),610					&b.get(field)?.expect("field exists"),611				)? {612					return Ok(false);613				}614			}615			Ok(true)616		}617		(a, b) => Ok(primitive_equals(a, b)?),618	}619}
after · crates/jrsonnet-evaluator/src/val.rs
1use std::{2	cell::RefCell,3	fmt::{self, Debug, Display},4	mem::replace,5	num::NonZeroU32,6	rc::Rc,7};89use jrsonnet_gcmodule::{Cc, Trace};10use jrsonnet_interner::IStr;11use jrsonnet_types::ValType;1213pub use crate::arr::{ArrValue, ArrayLike};14use crate::{15	bail,16	error::{Error, ErrorKind::*},17	function::FuncVal,18	gc::{GcHashMap, TraceBox},19	manifest::{ManifestFormat, ToStringFormat},20	tb,21	typed::BoundedUsize,22	ObjValue, Result, Unbound, WeakObjValue,23};2425pub trait ThunkValue: Trace {26	type Output;27	fn get(self: Box<Self>) -> Result<Self::Output>;28}2930#[derive(Trace)]31enum ThunkInner<T: Trace> {32	Computed(T),33	Errored(Error),34	Waiting(TraceBox<dyn ThunkValue<Output = T>>),35	Pending,36}3738/// Lazily evaluated value39#[allow(clippy::module_name_repetitions)]40#[derive(Clone, Trace)]41pub struct Thunk<T: Trace>(Cc<RefCell<ThunkInner<T>>>);4243impl<T: Trace> Thunk<T> {44	pub fn evaluated(val: T) -> Self {45		Self(Cc::new(RefCell::new(ThunkInner::Computed(val))))46	}47	pub fn new(f: impl ThunkValue<Output = T> + 'static) -> Self {48		Self(Cc::new(RefCell::new(ThunkInner::Waiting(tb!(f)))))49	}50	pub fn errored(e: Error) -> Self {51		Self(Cc::new(RefCell::new(ThunkInner::Errored(e))))52	}53	pub fn result(res: Result<T, Error>) -> Self {54		match res {55			Ok(o) => Self::evaluated(o),56			Err(e) => Self::errored(e),57		}58	}59}6061impl<T> Thunk<T>62where63	T: Clone + Trace,64{65	pub fn force(&self) -> Result<()> {66		self.evaluate()?;67		Ok(())68	}6970	/// Evaluate thunk, or return cached value71	///72	/// # Errors73	///74	/// - Lazy value evaluation returned error75	/// - This method was called during inner value evaluation76	pub fn evaluate(&self) -> Result<T> {77		match &*self.0.borrow() {78			ThunkInner::Computed(v) => return Ok(v.clone()),79			ThunkInner::Errored(e) => return Err(e.clone()),80			ThunkInner::Pending => return Err(InfiniteRecursionDetected.into()),81			ThunkInner::Waiting(..) => (),82		};83		let ThunkInner::Waiting(value) = replace(&mut *self.0.borrow_mut(), ThunkInner::Pending)84		else {85			unreachable!();86		};87		let new_value = match value.0.get() {88			Ok(v) => v,89			Err(e) => {90				*self.0.borrow_mut() = ThunkInner::Errored(e.clone());91				return Err(e);92			}93		};94		*self.0.borrow_mut() = ThunkInner::Computed(new_value.clone());95		Ok(new_value)96	}97}9899pub trait ThunkMapper<Input>: Trace {100	type Output;101	fn map(self, from: Input) -> Result<Self::Output>;102}103impl<Input> Thunk<Input>104where105	Input: Trace + Clone,106{107	pub fn map<M>(self, mapper: M) -> Thunk<M::Output>108	where109		M: ThunkMapper<Input>,110		M::Output: Trace,111	{112		#[derive(Trace)]113		struct Mapped<Input: Trace, Mapper: Trace> {114			inner: Thunk<Input>,115			mapper: Mapper,116		}117		impl<Input, Mapper> ThunkValue for Mapped<Input, Mapper>118		where119			Input: Trace + Clone,120			Mapper: ThunkMapper<Input>,121		{122			type Output = Mapper::Output;123124			fn get(self: Box<Self>) -> Result<Self::Output> {125				let value = self.inner.evaluate()?;126				let mapped = self.mapper.map(value)?;127				Ok(mapped)128			}129		}130131		Thunk::new(Mapped::<Input, M> {132			inner: self,133			mapper,134		})135	}136}137138impl<T: Trace> From<Result<T>> for Thunk<T> {139	fn from(value: Result<T>) -> Self {140		match value {141			Ok(o) => Self::evaluated(o),142			Err(e) => Self::errored(e),143		}144	}145}146impl<T, V: Trace> From<T> for Thunk<V>147where148	T: ThunkValue<Output = V>,149{150	fn from(value: T) -> Self {151		Self::new(value)152	}153}154155impl<T: Trace + Default> Default for Thunk<T> {156	fn default() -> Self {157		Self::evaluated(T::default())158	}159}160161type CacheKey = (Option<WeakObjValue>, Option<WeakObjValue>);162163#[derive(Trace, Clone)]164pub struct CachedUnbound<I, T>165where166	I: Unbound<Bound = T>,167	T: Trace,168{169	cache: Cc<RefCell<GcHashMap<CacheKey, T>>>,170	value: I,171}172impl<I: Unbound<Bound = T>, T: Trace> CachedUnbound<I, T> {173	pub fn new(value: I) -> Self {174		Self {175			cache: Cc::new(RefCell::new(GcHashMap::new())),176			value,177		}178	}179}180impl<I: Unbound<Bound = T>, T: Clone + Trace> Unbound for CachedUnbound<I, T> {181	type Bound = T;182	fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<T> {183		let cache_key = (184			sup.as_ref().map(|s| s.clone().downgrade()),185			this.as_ref().map(|t| t.clone().downgrade()),186		);187		{188			if let Some(t) = self.cache.borrow().get(&cache_key) {189				return Ok(t.clone());190			}191		}192		let bound = self.value.bind(sup, this)?;193194		{195			let mut cache = self.cache.borrow_mut();196			cache.insert(cache_key, bound.clone());197		}198199		Ok(bound)200	}201}202203impl<T: Debug + Trace> Debug for Thunk<T> {204	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {205		write!(f, "Lazy")206	}207}208impl<T: Trace> PartialEq for Thunk<T> {209	fn eq(&self, other: &Self) -> bool {210		Cc::ptr_eq(&self.0, &other.0)211	}212}213214/// Represents a Jsonnet value, which can be sliced or indexed (string or array).215#[allow(clippy::module_name_repetitions)]216pub enum IndexableVal {217	/// String.218	Str(IStr),219	/// Array.220	Arr(ArrValue),221}222impl IndexableVal {223	pub fn is_empty(&self) -> bool {224		match self {225			Self::Str(s) => s.is_empty(),226			Self::Arr(s) => s.is_empty(),227		}228	}229230	pub fn to_array(self) -> ArrValue {231		match self {232			Self::Str(s) => ArrValue::chars(s.chars()),233			Self::Arr(arr) => arr,234		}235	}236	/// Slice the value.237	///238	/// # Implementation239	///240	/// For strings, will create a copy of specified interval.241	///242	/// For arrays, nothing will be copied on this call, instead [`ArrValue::Slice`] view will be returned.243	pub fn slice(244		self,245		index: Option<i32>,246		end: Option<i32>,247		step: Option<BoundedUsize<1, { i32::MAX as usize }>>,248	) -> Result<Self> {249		match &self {250			Self::Str(s) => {251				let mut computed_len = None;252				let mut get_len = || {253					computed_len.map_or_else(254						|| {255							let len = s.chars().count();256							let _ = computed_len.insert(len);257							len258						},259						|len| len,260					)261				};262				let mut get_idx = |pos: Option<i32>, default| {263					match pos {264						Some(v) if v < 0 => get_len().saturating_sub((-v) as usize),265						// No need to clamp, as iterator interface is used266						Some(v) => v as usize,267						None => default,268					}269				};270271				let index = get_idx(index, 0);272				let end = get_idx(end, usize::MAX);273				let step = step.as_deref().copied().unwrap_or(1);274275				if index >= end {276					return Ok(Self::Str("".into()));277				}278279				Ok(Self::Str(280					(s.chars()281						.skip(index)282						.take(end - index)283						.step_by(step)284						.collect::<String>())285					.into(),286				))287			}288			Self::Arr(arr) => Ok(Self::Arr(arr.clone().slice(289				index,290				end,291				step.map(|v| NonZeroU32::new(v.value() as u32).expect("bounded != 0")),292			))),293		}294	}295}296297#[derive(Debug, Clone, Trace)]298pub enum StrValue {299	Flat(IStr),300	Tree(Rc<(StrValue, StrValue, usize)>),301}302impl StrValue {303	pub fn concat(a: Self, b: Self) -> Self {304		// TODO: benchmark for an optimal value, currently just a arbitrary choice305		const STRING_EXTEND_THRESHOLD: usize = 100;306307		if a.is_empty() {308			b309		} else if b.is_empty() {310			a311		} else if a.len() + b.len() < STRING_EXTEND_THRESHOLD {312			Self::Flat(format!("{a}{b}").into())313		} else {314			let len = a.len() + b.len();315			Self::Tree(Rc::new((a, b, len)))316		}317	}318	pub fn into_flat(self) -> IStr {319		#[cold]320		fn write_buf(s: &StrValue, out: &mut String) {321			match s {322				StrValue::Flat(f) => out.push_str(f),323				StrValue::Tree(t) => {324					write_buf(&t.0, out);325					write_buf(&t.1, out);326				}327			}328		}329		match self {330			Self::Flat(f) => f,331			Self::Tree(_) => {332				let mut buf = String::with_capacity(self.len());333				write_buf(&self, &mut buf);334				buf.into()335			}336		}337	}338	pub fn len(&self) -> usize {339		match self {340			Self::Flat(v) => v.len(),341			Self::Tree(t) => t.2,342		}343	}344	pub fn is_empty(&self) -> bool {345		match self {346			Self::Flat(v) => v.is_empty(),347			// Can't create non-flat empty string348			Self::Tree(_) => false,349		}350	}351}352impl<T> From<T> for StrValue353where354	IStr: From<T>,355{356	fn from(value: T) -> Self {357		Self::Flat(IStr::from(value))358	}359}360impl Display for StrValue {361	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {362		match self {363			Self::Flat(v) => write!(f, "{v}"),364			Self::Tree(t) => {365				write!(f, "{}", t.0)?;366				write!(f, "{}", t.1)367			}368		}369	}370}371impl PartialEq for StrValue {372	// False positive, into_flat returns not StrValue, but IStr, thus no infinite recursion here.373	#[allow(clippy::unconditional_recursion)]374	fn eq(&self, other: &Self) -> bool {375		let a = self.clone().into_flat();376		let b = other.clone().into_flat();377		a == b378	}379}380impl Eq for StrValue {}381impl PartialOrd for StrValue {382	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {383		Some(self.cmp(other))384	}385}386impl Ord for StrValue {387	fn cmp(&self, other: &Self) -> std::cmp::Ordering {388		let a = self.clone().into_flat();389		let b = other.clone().into_flat();390		a.cmp(&b)391	}392}393394/// Represents any valid Jsonnet value.395#[derive(Debug, Clone, Trace, Default)]396pub enum Val {397	/// Represents a Jsonnet boolean.398	Bool(bool),399	/// Represents a Jsonnet null value.400	#[default]401	Null,402	/// Represents a Jsonnet string.403	Str(StrValue),404	/// Represents a Jsonnet number.405	/// Should be finite, and not NaN406	/// This restriction isn't enforced by enum, as enum field can't be marked as private407	Num(f64),408	/// Experimental bigint409	#[cfg(feature = "exp-bigint")]410	BigInt(#[trace(skip)] Box<num_bigint::BigInt>),411	/// Represents a Jsonnet array.412	Arr(ArrValue),413	/// Represents a Jsonnet object.414	Obj(ObjValue),415	/// Represents a Jsonnet function.416	Func(FuncVal),417}418419#[cfg(target_pointer_width = "64")]420static_assertions::assert_eq_size!(Val, [u8; 24]);421422impl From<IndexableVal> for Val {423	fn from(v: IndexableVal) -> Self {424		match v {425			IndexableVal::Str(s) => Self::string(s),426			IndexableVal::Arr(a) => Self::Arr(a),427		}428	}429}430431impl Val {432	pub const fn as_bool(&self) -> Option<bool> {433		match self {434			Self::Bool(v) => Some(*v),435			_ => None,436		}437	}438	pub const fn as_null(&self) -> Option<()> {439		match self {440			Self::Null => Some(()),441			_ => None,442		}443	}444	pub fn as_str(&self) -> Option<IStr> {445		match self {446			Self::Str(s) => Some(s.clone().into_flat()),447			_ => None,448		}449	}450	pub const fn as_num(&self) -> Option<f64> {451		match self {452			Self::Num(n) => Some(*n),453			_ => None,454		}455	}456	pub fn as_arr(&self) -> Option<ArrValue> {457		match self {458			Self::Arr(a) => Some(a.clone()),459			_ => None,460		}461	}462	pub fn as_obj(&self) -> Option<ObjValue> {463		match self {464			Self::Obj(o) => Some(o.clone()),465			_ => None,466		}467	}468	pub fn as_func(&self) -> Option<FuncVal> {469		match self {470			Self::Func(f) => Some(f.clone()),471			_ => None,472		}473	}474475	/// Creates `Val::Num` after checking for numeric overflow.476	/// As numbers are `f64`, we can just check for their finity.477	pub fn new_checked_num(num: f64) -> Result<Self> {478		if num.is_finite() {479			Ok(Self::Num(num))480		} else {481			bail!("overflow")482		}483	}484485	pub const fn value_type(&self) -> ValType {486		match self {487			Self::Str(..) => ValType::Str,488			Self::Num(..) => ValType::Num,489			#[cfg(feature = "exp-bigint")]490			Self::BigInt(..) => ValType::BigInt,491			Self::Arr(..) => ValType::Arr,492			Self::Obj(..) => ValType::Obj,493			Self::Bool(_) => ValType::Bool,494			Self::Null => ValType::Null,495			Self::Func(..) => ValType::Func,496		}497	}498499	pub fn manifest(&self, format: impl ManifestFormat) -> Result<String> {500		fn manifest_dyn(val: &Val, manifest: &dyn ManifestFormat) -> Result<String> {501			manifest.manifest(val.clone())502		}503		manifest_dyn(self, &format)504	}505506	pub fn to_string(&self) -> Result<IStr> {507		Ok(match self {508			Self::Bool(true) => "true".into(),509			Self::Bool(false) => "false".into(),510			Self::Null => "null".into(),511			Self::Str(s) => s.clone().into_flat(),512			_ => self.manifest(ToStringFormat).map(IStr::from)?,513		})514	}515516	pub fn into_indexable(self) -> Result<IndexableVal> {517		Ok(match self {518			Self::Str(s) => IndexableVal::Str(s.into_flat()),519			Self::Arr(arr) => IndexableVal::Arr(arr),520			_ => bail!(ValueIsNotIndexable(self.value_type())),521		})522	}523524	pub fn function(function: impl Into<FuncVal>) -> Self {525		Self::Func(function.into())526	}527	pub fn string(string: impl Into<StrValue>) -> Self {528		Self::Str(string.into())529	}530}531532impl From<IStr> for Val {533	fn from(value: IStr) -> Self {534		Self::string(value)535	}536}537impl From<String> for Val {538	fn from(value: String) -> Self {539		Self::string(value)540	}541}542impl From<&str> for Val {543	fn from(value: &str) -> Self {544		Self::string(value)545	}546}547impl From<ObjValue> for Val {548	fn from(value: ObjValue) -> Self {549		Self::Obj(value)550	}551}552553const fn is_function_like(val: &Val) -> bool {554	matches!(val, Val::Func(_))555}556557/// Native implementation of `std.primitiveEquals`558pub fn primitive_equals(val_a: &Val, val_b: &Val) -> Result<bool> {559	Ok(match (val_a, val_b) {560		(Val::Bool(a), Val::Bool(b)) => a == b,561		(Val::Null, Val::Null) => true,562		(Val::Str(a), Val::Str(b)) => a == b,563		(Val::Num(a), Val::Num(b)) => (a - b).abs() <= f64::EPSILON,564		#[cfg(feature = "exp-bigint")]565		(Val::BigInt(a), Val::BigInt(b)) => a == b,566		(Val::Arr(_), Val::Arr(_)) => {567			bail!("primitiveEquals operates on primitive types, got array")568		}569		(Val::Obj(_), Val::Obj(_)) => {570			bail!("primitiveEquals operates on primitive types, got object")571		}572		(a, b) if is_function_like(a) && is_function_like(b) => {573			bail!("cannot test equality of functions")574		}575		(_, _) => false,576	})577}578579/// Native implementation of `std.equals`580pub fn equals(val_a: &Val, val_b: &Val) -> Result<bool> {581	if val_a.value_type() != val_b.value_type() {582		return Ok(false);583	}584	match (val_a, val_b) {585		(Val::Arr(a), Val::Arr(b)) => {586			if ArrValue::ptr_eq(a, b) {587				return Ok(true);588			}589			if a.len() != b.len() {590				return Ok(false);591			}592			for (a, b) in a.iter().zip(b.iter()) {593				if !equals(&a?, &b?)? {594					return Ok(false);595				}596			}597			Ok(true)598		}599		(Val::Obj(a), Val::Obj(b)) => {600			if ObjValue::ptr_eq(a, b) {601				return Ok(true);602			}603			let fields = a.fields(604				#[cfg(feature = "exp-preserve-order")]605				false,606			);607			if fields608				!= b.fields(609					#[cfg(feature = "exp-preserve-order")]610					false,611				) {612				return Ok(false);613			}614			for field in fields {615				if !equals(616					&a.get(field.clone())?.expect("field exists"),617					&b.get(field)?.expect("field exists"),618				)? {619					return Ok(false);620				}621			}622			Ok(true)623		}624		(a, b) => Ok(primitive_equals(a, b)?),625	}626}
modifiedcrates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/lib.rs
+++ b/crates/jrsonnet-stdlib/src/lib.rs
@@ -201,6 +201,9 @@
 		("parseOctal", builtin_parse_octal::INST),
 		("parseHex", builtin_parse_hex::INST),
 		("stringChars", builtin_string_chars::INST),
+		("lstripChars", builtin_lstrip_chars::INST),
+		("rstripChars", builtin_rstrip_chars::INST),
+		("stripChars", builtin_strip_chars::INST),
 		// Misc
 		("length", builtin_length::INST),
 		("get", builtin_get::INST),
modifiedcrates/jrsonnet-stdlib/src/std.jsonnetdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/std.jsonnet
+++ b/crates/jrsonnet-stdlib/src/std.jsonnet
@@ -3,22 +3,6 @@
 
   thisFile:: error 'std.thisFile is deprecated, to enable its support in jrsonnet - recompile it with "legacy-this-file" support.\nThis will slow down stdlib caching a bit, though',
 
-  lstripChars(str, chars)::
-    if std.length(str) > 0 && std.member(chars, str[0]) then
-      std.lstripChars(str[1:], chars)
-    else
-      str,
-
-  rstripChars(str, chars)::
-    local len = std.length(str);
-    if len > 0 && std.member(chars, str[len - 1]) then
-      std.rstripChars(str[:len - 1], chars)
-    else
-      str,
-
-  stripChars(str, chars)::
-    std.lstripChars(std.rstripChars(str, chars), chars),
-
   mapWithIndex(func, arr)::
     if !std.isFunction(func) then
       error ('std.mapWithIndex first param must be function, got ' + std.type(func))
modifiedcrates/jrsonnet-stdlib/src/strings.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/strings.rs
+++ b/crates/jrsonnet-stdlib/src/strings.rs
@@ -1,9 +1,11 @@
+use std::collections::BTreeSet;
+
 use jrsonnet_evaluator::{
 	bail,
 	error::{ErrorKind::*, Result},
 	function::builtin,
-	typed::{Either2, M1},
-	val::ArrValue,
+	typed::{Either2, Typed, M1},
+	val::{ArrValue, IndexableVal},
 	Either, IStr, Val,
 };
 
@@ -215,6 +217,53 @@
 	})
 }
 
+#[builtin]
+pub fn builtin_string_chars(str: IStr) -> ArrValue {
+	ArrValue::chars(str.chars())
+}
+
+#[builtin]
+pub fn builtin_lstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {
+	if str.is_empty() || chars.is_empty() {
+		return Ok(str);
+	}
+
+	let pattern = new_trim_pattern(chars)?;
+	Ok(str.as_str().trim_start_matches(pattern).into())
+}
+
+#[builtin]
+pub fn builtin_rstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {
+	if str.is_empty() || chars.is_empty() {
+		return Ok(str);
+	}
+
+	let pattern = new_trim_pattern(chars)?;
+	Ok(str.as_str().trim_end_matches(pattern).into())
+}
+
+#[builtin]
+pub fn builtin_strip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {
+	if str.is_empty() || chars.is_empty() {
+		return Ok(str);
+	}
+
+	let pattern = new_trim_pattern(chars)?;
+	Ok(str.as_str().trim_matches(pattern).into())
+}
+
+fn new_trim_pattern(chars: IndexableVal) -> Result<impl Fn(char) -> bool> {
+	let chars: BTreeSet<char> = match chars {
+		IndexableVal::Str(chars) => chars.chars().collect(),
+		IndexableVal::Arr(chars) => chars
+			.iter()
+			.filter_map(|it| it.map(|it| char::from_untyped(it).ok()).transpose())
+			.collect::<Result<_, _>>()?,
+	};
+
+	Ok(move |char| chars.contains(&char))
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
@@ -242,9 +291,4 @@
 		assert_eq!(parse_nat::<16>("a9").unwrap(), 0xA9 as f64);
 		assert_eq!(parse_nat::<16>("BbC").unwrap(), 0xBBC as f64);
 	}
-}
-
-#[builtin]
-pub fn builtin_string_chars(str: IStr) -> ArrValue {
-	ArrValue::chars(str.chars())
 }
modifiedtests/Cargo.tomldiffbeforeafterboth
--- a/tests/Cargo.toml
+++ b/tests/Cargo.toml
@@ -12,3 +12,5 @@
 jrsonnet-gcmodule.workspace = true
 jrsonnet-stdlib.workspace = true
 serde.workspace = true
+json-structural-diff.workspace = true
+serde_json.workspace = true
addedtests/golden/builtin_strings_string.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/tests/golden/builtin_strings_string.jsonnet
@@ -0,0 +1,21 @@
+{
+	lstripChars_singleChar: std.lstripChars("aaabcdef", "a"),
+	lstripChars_multipleChars: std.lstripChars("klmn", "kql"),
+	lstripChars_array: std.lstripChars("forward", [1, "f", [], "o", "d", "for"]),
+
+	rstripChars_singleChar: std.rstripChars("nice_boy", "y"),
+	rstripChars_multipleChars: std.rstripChars("amoguass", "sa"),
+	rstripChars_array: std.rstripChars("cool just cool", ["o", "l", 12.2323443]),
+
+	stripChars_singleCharL: std.stripChars("feefoofaa", "f"),
+	stripChars_singleCharR: std.stripChars("lolkekw", "w"),
+	stripChars_singleChar: std.stripChars("joper jej", "j"),
+
+	stripChars_multipleCharsL: std.stripChars("abcdefg", "cab"),
+	stripChars_multipleCharsR: std.stripChars("still breathing", "gthin"),
+	stripChars_multipleChars: std.stripChars("sus sus sus", "us"),
+
+	stripChars_arrayL: std.stripChars("chel medvedo svin", ["c", 3204990, {"svin": {}}, "vi"]),
+	stripChars_arrayR: std.stripChars("lach-vs-miri", ["r", "i", "craft", "is", "mine"]),
+	stripChars_array: std.stripChars("UwU Lel Stosh", ["h", "U", "s", {}, [], null, "w", [1, 2, 3]]),
+}
addedtests/golden/builtin_strings_string.jsonnet.goldendiffbeforeafterboth
--- /dev/null
+++ b/tests/golden/builtin_strings_string.jsonnet.golden
@@ -0,0 +1,17 @@
+{
+    "lstripChars_array": "rward",
+    "lstripChars_multipleChars": "mn",
+    "lstripChars_singleChar": "bcdef",
+    "rstripChars_array": "cool just c",
+    "rstripChars_multipleChars": "amogu",
+    "rstripChars_singleChar": "nice_bo",
+    "stripChars_array": " Lel Sto",
+    "stripChars_arrayL": "hel medvedo svin",
+    "stripChars_arrayR": "lach-vs-m",
+    "stripChars_multipleChars": " sus ",
+    "stripChars_multipleCharsL": "defg",
+    "stripChars_multipleCharsR": "still brea",
+    "stripChars_singleChar": "oper je",
+    "stripChars_singleCharL": "eefoofaa",
+    "stripChars_singleCharR": "lolkek"
+}
\ No newline at end of file
modifiedtests/tests/golden.rsdiffbeforeafterboth
--- a/tests/tests/golden.rs
+++ b/tests/tests/golden.rs
@@ -9,7 +9,6 @@
 	FileImportResolver, State,
 };
 use jrsonnet_stdlib::StateExt;
-
 mod common;
 
 fn run(file: &Path) -> String {
@@ -35,6 +34,8 @@
 
 #[test]
 fn test() -> io::Result<()> {
+	use json_structural_diff::JsonDiff;
+
 	let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
 	root.push("golden");
 
@@ -54,6 +55,35 @@
 		} else {
 			let golden = fs::read_to_string(golden_path)?;
 
+			match (serde_json::from_str(&result), serde_json::from_str(&golden)) {
+				(Err(_), Ok(_)) => assert_eq!(
+					result,
+					golden,
+					"unexpected error for golden {}",
+					entry.path().display()
+				),
+				(Ok(_), Err(_)) => assert_eq!(
+					result,
+					golden,
+					"expected error for golden {}",
+					entry.path().display()
+				),
+				(Ok(result), Ok(golden)) => {
+					// Show diff relative to golden`.
+					let diff = JsonDiff::diff_string(&golden, &result, false);
+					if let Some(diff) = diff {
+						panic!(
+							"Result \n{result:#}\n\
+								and golden \n{golden:#}\n\
+								did not match structurally:\n{diff:#}\n\
+								for golden {}",
+							entry.path().display()
+						);
+					}
+				}
+				(Err(_), Err(_)) => {}
+			};
+
 			assert_eq!(
 				result,
 				golden,