git.delta.rocks / jrsonnet / refs/commits / 2afd5ff0dd7a

difftreelog

refactor extended strings

Yaroslav Bolyukin2022-12-03parent: #81f0998.patch.diff
in: master

16 files changed

modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -17,7 +17,7 @@
 	function::{CallLocation, FuncDesc, FuncVal},
 	tb, throw,
 	typed::Typed,
-	val::{CachedUnbound, IndexableVal, Thunk, ThunkValue},
+	val::{CachedUnbound, IndexableVal, StrValue, Thunk, ThunkValue},
 	Context, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, State,
 	Unbound, Val,
 };
@@ -36,7 +36,7 @@
 		}
 	}
 	Some(match &*expr.0 {
-		Expr::Str(s) => Val::Str(s.clone()),
+		Expr::Str(s) => Val::Str(StrValue::Flat(s.clone())),
 		Expr::Num(n) => Val::Num(*n),
 		Expr::Literal(LiteralType::False) => Val::Bool(false),
 		Expr::Literal(LiteralType::True) => Val::Bool(true),
@@ -135,7 +135,7 @@
 					let fctx = Pending::new();
 					let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());
 					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(Cc::new(vec![
-						Thunk::evaluated(Val::Str(field.clone())),
+						Thunk::evaluated(Val::Str(StrValue::Flat(field.clone()))),
 						Thunk::new(tb!(ObjectFieldThunk {
 							field: field.clone(),
 							obj: obj.clone(),
@@ -436,7 +436,7 @@
 		Literal(LiteralType::False) => Val::Bool(false),
 		Literal(LiteralType::Null) => Val::Null,
 		Parened(e) => evaluate(ctx, e)?,
-		Str(v) => Val::Str(v.clone()),
+		Str(v) => Val::Str(StrValue::Flat(v.clone())),
 		Num(v) => Val::new_checked_num(*v)?,
 		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,
 		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,
@@ -457,14 +457,14 @@
 			ctx.super_obj()
 				.clone()
 				.expect("no super found")
-				.get_for(name, ctx.this().clone().expect("no this found"))?
+				.get_for(name.into_flat(), ctx.this().clone().expect("no this found"))?
 				.expect("value not found")
 		}
 		Index(value, index) => match (evaluate(ctx.clone(), value)?, evaluate(ctx, index)?) {
 			(Val::Obj(v), Val::Str(key)) => State::push(
 				CallLocation::new(loc),
 				|| format!("field <{key}> access"),
-				|| match v.get(key.clone()) {
+				|| match v.get(key.clone().into_flat()) {
 					Ok(Some(v)) => Ok(v),
 					#[cfg(not(feature = "friendly-errors"))]
 					Ok(None) => throw!(NoSuchField(key.clone(), vec![])),
@@ -476,7 +476,10 @@
 							#[cfg(feature = "exp-preserve-order")]
 							false,
 						) {
-							let conf = strsim::jaro_winkler(&field as &str, &key as &str);
+							let conf = strsim::jaro_winkler(
+								&field as &str,
+								&key.clone().into_flat() as &str,
+							);
 							if conf < 0.8 {
 								continue;
 							}
@@ -485,7 +488,7 @@
 						heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
 
 						throw!(NoSuchField(
-							key.clone(),
+							key.clone().into_flat(),
 							heap.into_iter().map(|(_, v)| v).collect()
 						))
 					}
@@ -505,7 +508,7 @@
 				v.get(n as usize)?
 					.ok_or_else(|| ArrayBoundsError(n as usize, v.len()))?
 			}
-			(Val::Arr(_), Val::Str(n)) => throw!(AttemptedIndexAnArrayWithString(n)),
+			(Val::Arr(_), Val::Str(n)) => throw!(AttemptedIndexAnArrayWithString(n.into_flat())),
 			(Val::Arr(_), n) => throw!(ValueIndexMustBeTypeGot(
 				ValType::Arr,
 				ValType::Num,
@@ -514,16 +517,18 @@
 
 			(Val::Str(s), Val::Num(n)) => Val::Str({
 				let v: IStr = s
+					.clone()
+					.into_flat()
 					.chars()
 					.skip(n as usize)
 					.take(1)
 					.collect::<String>()
 					.into();
 				if v.is_empty() {
-					let size = s.chars().count();
+					let size = s.into_flat().chars().count();
 					throw!(StringBoundsError(n as usize, size))
 				}
-				v
+				StrValue::Flat(v)
 			}),
 			(Val::Str(_), n) => throw!(ValueIndexMustBeTypeGot(
 				ValType::Str,
@@ -654,7 +659,7 @@
 					|| format!("import {:?}", path.clone()),
 					|| s.import_resolved(resolved_path),
 				)?,
-				ImportStr(_) => Val::Str(s.import_resolved_str(resolved_path)?),
+				ImportStr(_) => Val::Str(StrValue::Flat(s.import_resolved_str(resolved_path)?)),
 				ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)),
 				_ => unreachable!(),
 			}
modifiedcrates/jrsonnet-evaluator/src/evaluate/operator.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/operator.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/operator.rs
@@ -3,8 +3,14 @@
 use jrsonnet_parser::{BinaryOpType, LocExpr, UnaryOpType};
 
 use crate::{
-	arr::ArrValue, error::ErrorKind::*, evaluate, stdlib::std_format, throw, typed::Typed,
-	val::equals, Context, Result, Val,
+	arr::ArrValue,
+	error::ErrorKind::*,
+	evaluate,
+	stdlib::std_format,
+	throw,
+	typed::Typed,
+	val::{equals, StrValue},
+	Context, Result, Val,
 };
 
 pub fn evaluate_unary_op(op: UnaryOpType, b: &Val) -> Result<Val> {
@@ -25,15 +31,21 @@
 	Ok(match (a, b) {
 		(Str(a), Str(b)) if a.is_empty() => Val::Str(b.clone()),
 		(Str(a), Str(b)) if b.is_empty() => Val::Str(a.clone()),
-		(Str(v1), Str(v2)) => Str(((**v1).to_owned() + v2).into()),
+		(Str(v1), Str(v2)) => Str(StrValue::concat(v1.clone(), v2.clone())),
 
 		// Can't use generic json serialization way, because it depends on number to string concatenation (std.jsonnet:890)
-		(Num(a), Str(b)) => Str(format!("{a}{b}").into()),
-		(Str(a), Num(b)) => Str(format!("{a}{b}").into()),
+		(Num(a), Str(b)) => Str(StrValue::Flat(format!("{a}{b}").into())),
+		(Str(a), Num(b)) => Str(StrValue::Flat(format!("{a}{b}").into())),
 
-		(Str(a), o) | (o, Str(a)) if a.is_empty() => Val::Str(o.clone().to_string()?),
-		(Str(a), o) => Str(format!("{a}{}", o.clone().to_string()?).into()),
-		(o, Str(a)) => Str(format!("{}{a}", o.clone().to_string()?).into()),
+		(Str(a), o) | (o, Str(a)) if a.is_empty() => {
+			Val::Str(StrValue::Flat(o.clone().to_string()?))
+		}
+		(Str(a), o) => Str(StrValue::Flat(
+			format!("{a}{}", o.clone().to_string()?).into(),
+		)),
+		(o, Str(a)) => Str(StrValue::Flat(
+			format!("{}{a}", o.clone().to_string()?).into(),
+		)),
 
 		(Obj(v1), Obj(v2)) => Obj(v2.extend_from(v1.clone())),
 		(Arr(a), Arr(b)) => Val::Arr(ArrValue::extended(a.clone(), b.clone())),
@@ -56,7 +68,9 @@
 			}
 			Ok(Num(a % b))
 		}
-		(Str(str), vals) => String::into_untyped(std_format(str.clone(), vals.clone())?),
+		(Str(str), vals) => {
+			String::into_untyped(std_format(&str.clone().into_flat(), vals.clone())?)
+		}
 		(a, b) => throw!(BinaryOperatorDoesNotOperateOnValues(
 			BinaryOpType::Mod,
 			a.value_type(),
@@ -120,10 +134,10 @@
 		(a, Lte, b) => Bool(evaluate_compare_op(a, b, Lte)?.is_le()),
 		(a, Gte, b) => Bool(evaluate_compare_op(a, b, Gte)?.is_ge()),
 
-		(Str(a), In, Obj(obj)) => Bool(obj.has_field_ex(a.clone(), true)),
+		(Str(a), In, Obj(obj)) => Bool(obj.has_field_ex(a.clone().into_flat(), true)),
 		(a, Mod, b) => evaluate_mod_op(a, b)?,
 
-		(Str(v1), Mul, Num(v2)) => Str(v1.repeat(*v2 as usize).into()),
+		(Str(v1), Mul, Num(v2)) => Str(StrValue::Flat(v1.to_string().repeat(*v2 as usize).into())),
 
 		// Bool X Bool
 		(Bool(a), And, Bool(b)) => Bool(*a && *b),
modifiedcrates/jrsonnet-evaluator/src/function/arglike.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/arglike.rs
+++ b/crates/jrsonnet-evaluator/src/function/arglike.rs
@@ -4,7 +4,13 @@
 use jrsonnet_parser::{ArgsDesc, LocExpr};
 
 use crate::{
-	error::Result, evaluate, gc::GcHashMap, tb, typed::Typed, val::ThunkValue, Context, Thunk, Val,
+	error::Result,
+	evaluate,
+	gc::GcHashMap,
+	tb,
+	typed::Typed,
+	val::{StrValue, ThunkValue},
+	Context, Thunk, Val,
 };
 
 /// Marker for arguments, which can be evaluated with context set to None
@@ -59,7 +65,7 @@
 impl ArgLike for TlaArg {
 	fn evaluate_arg(&self, ctx: Context, tailstrict: bool) -> Result<Thunk<Val>> {
 		match self {
-			TlaArg::String(s) => Ok(Thunk::evaluated(Val::Str(s.clone()))),
+			TlaArg::String(s) => Ok(Thunk::evaluated(Val::Str(StrValue::Flat(s.clone())))),
 			TlaArg::Code(code) => Ok(if tailstrict {
 				Thunk::evaluated(evaluate(ctx, code)?)
 			} else {
modifiedcrates/jrsonnet-evaluator/src/integrations/serde.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/integrations/serde.rs
+++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs
@@ -7,7 +7,7 @@
 	Deserialize, Serialize,
 };
 
-use crate::{arr::ArrValue, error::Result, ObjValueBuilder, State, Val};
+use crate::{arr::ArrValue, error::Result, val::StrValue, ObjValueBuilder, State, Val};
 
 impl<'de> Deserialize<'de> for Val {
 	fn deserialize<D>(deserializer: D) -> Result<Val, D::Error>
@@ -49,7 +49,7 @@
 			where
 				E: serde::de::Error,
 			{
-				Ok(Val::Str(v.into()))
+				Ok(Val::Str(StrValue::Flat(v.into())))
 			}
 
 			// visit_num! {
@@ -152,7 +152,7 @@
 		match self {
 			Val::Bool(v) => serializer.serialize_bool(*v),
 			Val::Null => serializer.serialize_none(),
-			Val::Str(s) => serializer.serialize_str(s),
+			Val::Str(s) => serializer.serialize_str(&s.clone().into_flat()),
 			Val::Num(n) => serializer.serialize_f64(*n),
 			Val::Arr(arr) => {
 				let mut seq = serializer.serialize_seq(Some(arr.len()))?;
modifiedcrates/jrsonnet-evaluator/src/manifest.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/manifest.rs
+++ b/crates/jrsonnet-evaluator/src/manifest.rs
@@ -147,7 +147,7 @@
 			}
 		}
 		Val::Null => buf.push_str("null"),
-		Val::Str(s) => escape_string_json_buf(s, buf),
+		Val::Str(s) => escape_string_json_buf(&s.clone().into_flat(), buf),
 		Val::Num(n) => write!(buf, "{n}").unwrap(),
 		Val::Arr(items) => {
 			buf.push('[');
@@ -256,7 +256,7 @@
 		let Val::Str(s) = val else {
 			throw!("output should be string for string manifest format, got {}", val.value_type())
 		};
-		out.write_str(&s).unwrap();
+		write!(out, "{s}").unwrap();
 		Ok(())
 	}
 }
modifiedcrates/jrsonnet-evaluator/src/stdlib/format.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/stdlib/format.rs
+++ b/crates/jrsonnet-evaluator/src/stdlib/format.rs
@@ -589,6 +589,7 @@
 					.ok_or_else(|| InvalidUnicodeCodepointGot(n as u32))?,
 			),
 			Val::Str(s) => {
+				let s = s.into_flat();
 				if s.chars().count() != 1 {
 					throw!("%c expected 1 char string, got {}", s.chars().count(),);
 				}
modifiedcrates/jrsonnet-evaluator/src/stdlib/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/stdlib/mod.rs
+++ b/crates/jrsonnet-evaluator/src/stdlib/mod.rs
@@ -2,13 +2,12 @@
 #![allow(clippy::unnecessary_wraps)]
 
 use format::{format_arr, format_obj};
-use jrsonnet_interner::IStr;
 
 use crate::{error::Result, function::CallLocation, State, Val};
 
 pub mod format;
 
-pub fn std_format(str: IStr, vals: Val) -> Result<String> {
+pub fn std_format(str: &str, vals: Val) -> Result<String> {
 	State::push(
 		CallLocation::native(),
 		|| format!("std.format of {str}"),
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -11,7 +11,7 @@
 	function::{native::NativeDesc, FuncDesc, FuncVal},
 	throw,
 	typed::CheckType,
-	val::IndexableVal,
+	val::{IndexableVal, StrValue},
 	ObjValue, ObjValueBuilder, Val,
 };
 
@@ -187,13 +187,13 @@
 	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);
 
 	fn into_untyped(value: Self) -> Result<Val> {
-		Ok(Val::Str(value))
+		Ok(Val::Str(StrValue::Flat(value)))
 	}
 
 	fn from_untyped(value: Val) -> Result<Self> {
 		<Self as Typed>::TYPE.check(&value)?;
 		match value {
-			Val::Str(s) => Ok(s),
+			Val::Str(s) => Ok(s.into_flat()),
 			_ => unreachable!(),
 		}
 	}
@@ -203,7 +203,7 @@
 	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);
 
 	fn into_untyped(value: Self) -> Result<Val> {
-		Ok(Val::Str(value.into()))
+		Ok(Val::Str(StrValue::Flat(value.into())))
 	}
 
 	fn from_untyped(value: Val) -> Result<Self> {
@@ -219,13 +219,13 @@
 	const TYPE: &'static ComplexValType = &ComplexValType::Char;
 
 	fn into_untyped(value: Self) -> Result<Val> {
-		Ok(Val::Str(value.to_string().into()))
+		Ok(Val::Str(StrValue::Flat(value.to_string().into())))
 	}
 
 	fn from_untyped(value: Val) -> Result<Self> {
 		<Self as Typed>::TYPE.check(&value)?;
 		match value {
-			Val::Str(s) => Ok(s.chars().next().unwrap()),
+			Val::Str(s) => Ok(s.into_flat().chars().next().unwrap()),
 			_ => unreachable!(),
 		}
 	}
@@ -480,7 +480,7 @@
 
 	fn into_untyped(value: Self) -> Result<Val> {
 		match value {
-			IndexableVal::Str(s) => Ok(Val::Str(s)),
+			IndexableVal::Str(s) => Ok(Val::Str(StrValue::Flat(s))),
 			IndexableVal::Arr(a) => Ok(Val::Arr(a)),
 		}
 	}
modifiedcrates/jrsonnet-evaluator/src/typed/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/mod.rs
+++ b/crates/jrsonnet-evaluator/src/typed/mod.rs
@@ -150,7 +150,7 @@
 			Self::Any => Ok(()),
 			Self::Simple(t) => t.check(value),
 			Self::Char => match value {
-				Val::Str(s) if s.len() == 1 || s.chars().count() == 1 => Ok(()),
+				Val::Str(s) if s.len() == 1 || s.clone().into_flat().chars().count() == 1 => Ok(()),
 				v => Err(TypeError::ExpectedGot(self.clone(), v.value_type()).into()),
 			},
 			Self::BoundedNumber(from, to) => {
modifiedcrates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/val.rs
1use std::{cell::RefCell, fmt::Debug, mem::replace};23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_types::ValType;67pub use crate::arr::ArrValue;8use crate::{9	error::{Error, ErrorKind::*},10	function::FuncVal,11	gc::{GcHashMap, TraceBox},12	manifest::{ManifestFormat, ToStringFormat},13	throw,14	typed::BoundedUsize,15	ObjValue, Result, Unbound, WeakObjValue,16};1718pub trait ThunkValue: Trace {19	type Output;20	fn get(self: Box<Self>) -> Result<Self::Output>;21}2223#[derive(Trace)]24enum ThunkInner<T: Trace> {25	Computed(T),26	Errored(Error),27	Waiting(TraceBox<dyn ThunkValue<Output = T>>),28	Pending,29}3031#[allow(clippy::module_name_repetitions)]32#[derive(Clone, Trace)]33pub struct Thunk<T: Trace>(Cc<RefCell<ThunkInner<T>>>);3435impl<T: Trace> Thunk<T> {36	pub fn evaluated(val: T) -> Self {37		Self(Cc::new(RefCell::new(ThunkInner::Computed(val))))38	}39	pub fn new(f: TraceBox<dyn ThunkValue<Output = T>>) -> Self {40		Self(Cc::new(RefCell::new(ThunkInner::Waiting(f))))41	}42	pub fn errored(e: Error) -> Self {43		Self(Cc::new(RefCell::new(ThunkInner::Errored(e))))44	}45}4647impl<T> Thunk<T>48where49	T: Clone + Trace,50{51	pub fn force(&self) -> Result<()> {52		self.evaluate()?;53		Ok(())54	}55	pub fn evaluate(&self) -> Result<T> {56		match &*self.0.borrow() {57			ThunkInner::Computed(v) => return Ok(v.clone()),58			ThunkInner::Errored(e) => return Err(e.clone()),59			ThunkInner::Pending => return Err(InfiniteRecursionDetected.into()),60			ThunkInner::Waiting(..) => (),61		};62		let ThunkInner::Waiting(value) = replace(&mut *self.0.borrow_mut(), ThunkInner::Pending) else {63			unreachable!();64		};65		let new_value = match value.0.get() {66			Ok(v) => v,67			Err(e) => {68				*self.0.borrow_mut() = ThunkInner::Errored(e.clone());69				return Err(e);70			}71		};72		*self.0.borrow_mut() = ThunkInner::Computed(new_value.clone());73		Ok(new_value)74	}75}7677type CacheKey = (Option<WeakObjValue>, Option<WeakObjValue>);7879#[derive(Trace, Clone)]80pub struct CachedUnbound<I, T>81where82	I: Unbound<Bound = T>,83	T: Trace,84{85	cache: Cc<RefCell<GcHashMap<CacheKey, T>>>,86	value: I,87}88impl<I: Unbound<Bound = T>, T: Trace> CachedUnbound<I, T> {89	pub fn new(value: I) -> Self {90		Self {91			cache: Cc::new(RefCell::new(GcHashMap::new())),92			value,93		}94	}95}96impl<I: Unbound<Bound = T>, T: Clone + Trace> Unbound for CachedUnbound<I, T> {97	type Bound = T;98	fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<T> {99		let cache_key = (100			sup.as_ref().map(|s| s.clone().downgrade()),101			this.as_ref().map(|t| t.clone().downgrade()),102		);103		{104			if let Some(t) = self.cache.borrow().get(&cache_key) {105				return Ok(t.clone());106			}107		}108		let bound = self.value.bind(sup, this)?;109110		{111			let mut cache = self.cache.borrow_mut();112			cache.insert(cache_key, bound.clone());113		}114115		Ok(bound)116	}117}118119impl<T: Debug + Trace> Debug for Thunk<T> {120	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {121		write!(f, "Lazy")122	}123}124impl<T: Trace> PartialEq for Thunk<T> {125	fn eq(&self, other: &Self) -> bool {126		Cc::ptr_eq(&self.0, &other.0)127	}128}129130/// Represents a Jsonnet value, which can be spliced or indexed (string or array).131#[allow(clippy::module_name_repetitions)]132pub enum IndexableVal {133	/// String.134	Str(IStr),135	/// Array.136	Arr(ArrValue),137}138impl IndexableVal {139	/// Slice the value.140	///141	/// # Implementation142	///143	/// For strings, will create a copy of specified interval.144	///145	/// For arrays, nothing will be copied on this call, instead [`ArrValue::Slice`] view will be returned.146	pub fn slice(147		self,148		index: Option<BoundedUsize<0, { i32::MAX as usize }>>,149		end: Option<BoundedUsize<0, { i32::MAX as usize }>>,150		step: Option<BoundedUsize<1, { i32::MAX as usize }>>,151	) -> Result<Self> {152		match &self {153			IndexableVal::Str(s) => {154				let index = index.as_deref().copied().unwrap_or(0);155				let end = end.as_deref().copied().unwrap_or(usize::MAX);156				let step = step.as_deref().copied().unwrap_or(1);157158				if index >= end {159					return Ok(Self::Str("".into()));160				}161162				Ok(Self::Str(163					(s.chars()164						.skip(index)165						.take(end - index)166						.step_by(step)167						.collect::<String>())168					.into(),169				))170			}171			IndexableVal::Arr(arr) => {172				let index = index.as_deref().copied().unwrap_or(0);173				let end = end.as_deref().copied().unwrap_or(usize::MAX).min(arr.len());174				let step = step.as_deref().copied().unwrap_or(1);175176				if index >= end {177					return Ok(Self::Arr(ArrValue::empty()));178				}179180				Ok(Self::Arr(181					arr.clone()182						.slice(Some(index), Some(end), Some(step))183						.expect("arguments checked"),184				))185			}186		}187	}188}189190/// Represents any valid Jsonnet value.191#[derive(Debug, Clone, Trace)]192pub enum Val {193	/// Represents a Jsonnet boolean.194	Bool(bool),195	/// Represents a Jsonnet null value.196	Null,197	/// Represents a Jsonnet string.198	Str(IStr),199	/// Represents a Jsonnet number.200	/// Should be finite, and not NaN201	/// This restriction isn't enforced by enum, as enum field can't be marked as private202	Num(f64),203	/// Represents a Jsonnet array.204	Arr(ArrValue),205	/// Represents a Jsonnet object.206	Obj(ObjValue),207	/// Represents a Jsonnet function.208	Func(FuncVal),209}210211impl From<IndexableVal> for Val {212	fn from(v: IndexableVal) -> Self {213		match v {214			IndexableVal::Str(s) => Self::Str(s),215			IndexableVal::Arr(a) => Self::Arr(a),216		}217	}218}219220impl Val {221	pub const fn as_bool(&self) -> Option<bool> {222		match self {223			Self::Bool(v) => Some(*v),224			_ => None,225		}226	}227	pub const fn as_null(&self) -> Option<()> {228		match self {229			Self::Null => Some(()),230			_ => None,231		}232	}233	pub fn as_str(&self) -> Option<IStr> {234		match self {235			Self::Str(s) => Some(s.clone()),236			_ => None,237		}238	}239	pub const fn as_num(&self) -> Option<f64> {240		match self {241			Self::Num(n) => Some(*n),242			_ => None,243		}244	}245	pub fn as_arr(&self) -> Option<ArrValue> {246		match self {247			Self::Arr(a) => Some(a.clone()),248			_ => None,249		}250	}251	pub fn as_obj(&self) -> Option<ObjValue> {252		match self {253			Self::Obj(o) => Some(o.clone()),254			_ => None,255		}256	}257	pub fn as_func(&self) -> Option<FuncVal> {258		match self {259			Self::Func(f) => Some(f.clone()),260			_ => None,261		}262	}263264	/// Creates `Val::Num` after checking for numeric overflow.265	/// As numbers are `f64`, we can just check for their finity.266	pub fn new_checked_num(num: f64) -> Result<Self> {267		if num.is_finite() {268			Ok(Self::Num(num))269		} else {270			throw!("overflow")271		}272	}273274	pub const fn value_type(&self) -> ValType {275		match self {276			Self::Str(..) => ValType::Str,277			Self::Num(..) => ValType::Num,278			Self::Arr(..) => ValType::Arr,279			Self::Obj(..) => ValType::Obj,280			Self::Bool(_) => ValType::Bool,281			Self::Null => ValType::Null,282			Self::Func(..) => ValType::Func,283		}284	}285286	pub fn manifest(&self, format: impl ManifestFormat) -> Result<String> {287		fn manifest_dyn(val: &Val, manifest: &dyn ManifestFormat) -> Result<String> {288			manifest.manifest(val.clone())289		}290		manifest_dyn(self, &format)291	}292293	pub fn to_string(&self) -> Result<IStr> {294		Ok(match self {295			Self::Bool(true) => "true".into(),296			Self::Bool(false) => "false".into(),297			Self::Null => "null".into(),298			Self::Str(s) => s.clone(),299			_ => self.manifest(ToStringFormat).map(IStr::from)?,300		})301	}302303	pub fn into_indexable(self) -> Result<IndexableVal> {304		Ok(match self {305			Val::Str(s) => IndexableVal::Str(s),306			Val::Arr(arr) => IndexableVal::Arr(arr),307			_ => throw!(ValueIsNotIndexable(self.value_type())),308		})309	}310}311312const fn is_function_like(val: &Val) -> bool {313	matches!(val, Val::Func(_))314}315316/// Native implementation of `std.primitiveEquals`317pub fn primitive_equals(val_a: &Val, val_b: &Val) -> Result<bool> {318	Ok(match (val_a, val_b) {319		(Val::Bool(a), Val::Bool(b)) => a == b,320		(Val::Null, Val::Null) => true,321		(Val::Str(a), Val::Str(b)) => a == b,322		(Val::Num(a), Val::Num(b)) => (a - b).abs() <= f64::EPSILON,323		(Val::Arr(_), Val::Arr(_)) => {324			throw!("primitiveEquals operates on primitive types, got array")325		}326		(Val::Obj(_), Val::Obj(_)) => {327			throw!("primitiveEquals operates on primitive types, got object")328		}329		(a, b) if is_function_like(a) && is_function_like(b) => {330			throw!("cannot test equality of functions")331		}332		(_, _) => false,333	})334}335336/// Native implementation of `std.equals`337pub fn equals(val_a: &Val, val_b: &Val) -> Result<bool> {338	if val_a.value_type() != val_b.value_type() {339		return Ok(false);340	}341	match (val_a, val_b) {342		(Val::Arr(a), Val::Arr(b)) => {343			if ArrValue::ptr_eq(a, b) {344				return Ok(true);345			}346			if a.len() != b.len() {347				return Ok(false);348			}349			for (a, b) in a.iter().zip(b.iter()) {350				if !equals(&a?, &b?)? {351					return Ok(false);352				}353			}354			Ok(true)355		}356		(Val::Obj(a), Val::Obj(b)) => {357			if ObjValue::ptr_eq(a, b) {358				return Ok(true);359			}360			let fields = a.fields(361				#[cfg(feature = "exp-preserve-order")]362				false,363			);364			if fields365				!= b.fields(366					#[cfg(feature = "exp-preserve-order")]367					false,368				) {369				return Ok(false);370			}371			for field in fields {372				if !equals(373					&a.get(field.clone())?.expect("field exists"),374					&b.get(field)?.expect("field exists"),375				)? {376					return Ok(false);377				}378			}379			Ok(true)380		}381		(a, b) => Ok(primitive_equals(a, b)?),382	}383}
after · crates/jrsonnet-evaluator/src/val.rs
1use std::{2	cell::RefCell,3	fmt::{self, Debug, Display},4	mem::replace,5	rc::Rc,6};78use jrsonnet_gcmodule::{Cc, Trace};9use jrsonnet_interner::IStr;10use jrsonnet_types::ValType;1112pub use crate::arr::ArrValue;13use crate::{14	error::{Error, ErrorKind::*},15	function::FuncVal,16	gc::{GcHashMap, TraceBox},17	manifest::{ManifestFormat, ToStringFormat},18	throw,19	typed::BoundedUsize,20	ObjValue, Result, Unbound, WeakObjValue,21};2223pub trait ThunkValue: Trace {24	type Output;25	fn get(self: Box<Self>) -> Result<Self::Output>;26}2728#[derive(Trace)]29enum ThunkInner<T: Trace> {30	Computed(T),31	Errored(Error),32	Waiting(TraceBox<dyn ThunkValue<Output = T>>),33	Pending,34}3536#[allow(clippy::module_name_repetitions)]37#[derive(Clone, Trace)]38pub struct Thunk<T: Trace>(Cc<RefCell<ThunkInner<T>>>);3940impl<T: Trace> Thunk<T> {41	pub fn evaluated(val: T) -> Self {42		Self(Cc::new(RefCell::new(ThunkInner::Computed(val))))43	}44	pub fn new(f: TraceBox<dyn ThunkValue<Output = T>>) -> Self {45		Self(Cc::new(RefCell::new(ThunkInner::Waiting(f))))46	}47	pub fn errored(e: Error) -> Self {48		Self(Cc::new(RefCell::new(ThunkInner::Errored(e))))49	}50}5152impl<T> Thunk<T>53where54	T: Clone + Trace,55{56	pub fn force(&self) -> Result<()> {57		self.evaluate()?;58		Ok(())59	}60	pub fn evaluate(&self) -> Result<T> {61		match &*self.0.borrow() {62			ThunkInner::Computed(v) => return Ok(v.clone()),63			ThunkInner::Errored(e) => return Err(e.clone()),64			ThunkInner::Pending => return Err(InfiniteRecursionDetected.into()),65			ThunkInner::Waiting(..) => (),66		};67		let ThunkInner::Waiting(value) = replace(&mut *self.0.borrow_mut(), ThunkInner::Pending) else {68			unreachable!();69		};70		let new_value = match value.0.get() {71			Ok(v) => v,72			Err(e) => {73				*self.0.borrow_mut() = ThunkInner::Errored(e.clone());74				return Err(e);75			}76		};77		*self.0.borrow_mut() = ThunkInner::Computed(new_value.clone());78		Ok(new_value)79	}80}8182type CacheKey = (Option<WeakObjValue>, Option<WeakObjValue>);8384#[derive(Trace, Clone)]85pub struct CachedUnbound<I, T>86where87	I: Unbound<Bound = T>,88	T: Trace,89{90	cache: Cc<RefCell<GcHashMap<CacheKey, T>>>,91	value: I,92}93impl<I: Unbound<Bound = T>, T: Trace> CachedUnbound<I, T> {94	pub fn new(value: I) -> Self {95		Self {96			cache: Cc::new(RefCell::new(GcHashMap::new())),97			value,98		}99	}100}101impl<I: Unbound<Bound = T>, T: Clone + Trace> Unbound for CachedUnbound<I, T> {102	type Bound = T;103	fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<T> {104		let cache_key = (105			sup.as_ref().map(|s| s.clone().downgrade()),106			this.as_ref().map(|t| t.clone().downgrade()),107		);108		{109			if let Some(t) = self.cache.borrow().get(&cache_key) {110				return Ok(t.clone());111			}112		}113		let bound = self.value.bind(sup, this)?;114115		{116			let mut cache = self.cache.borrow_mut();117			cache.insert(cache_key, bound.clone());118		}119120		Ok(bound)121	}122}123124impl<T: Debug + Trace> Debug for Thunk<T> {125	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {126		write!(f, "Lazy")127	}128}129impl<T: Trace> PartialEq for Thunk<T> {130	fn eq(&self, other: &Self) -> bool {131		Cc::ptr_eq(&self.0, &other.0)132	}133}134135/// Represents a Jsonnet value, which can be spliced or indexed (string or array).136#[allow(clippy::module_name_repetitions)]137pub enum IndexableVal {138	/// String.139	Str(IStr),140	/// Array.141	Arr(ArrValue),142}143impl IndexableVal {144	/// Slice the value.145	///146	/// # Implementation147	///148	/// For strings, will create a copy of specified interval.149	///150	/// For arrays, nothing will be copied on this call, instead [`ArrValue::Slice`] view will be returned.151	pub fn slice(152		self,153		index: Option<BoundedUsize<0, { i32::MAX as usize }>>,154		end: Option<BoundedUsize<0, { i32::MAX as usize }>>,155		step: Option<BoundedUsize<1, { i32::MAX as usize }>>,156	) -> Result<Self> {157		match &self {158			IndexableVal::Str(s) => {159				let index = index.as_deref().copied().unwrap_or(0);160				let end = end.as_deref().copied().unwrap_or(usize::MAX);161				let step = step.as_deref().copied().unwrap_or(1);162163				if index >= end {164					return Ok(Self::Str("".into()));165				}166167				Ok(Self::Str(168					(s.chars()169						.skip(index)170						.take(end - index)171						.step_by(step)172						.collect::<String>())173					.into(),174				))175			}176			IndexableVal::Arr(arr) => {177				let index = index.as_deref().copied().unwrap_or(0);178				let end = end.as_deref().copied().unwrap_or(usize::MAX).min(arr.len());179				let step = step.as_deref().copied().unwrap_or(1);180181				if index >= end {182					return Ok(Self::Arr(ArrValue::empty()));183				}184185				Ok(Self::Arr(186					arr.clone()187						.slice(Some(index), Some(end), Some(step))188						.expect("arguments checked"),189				))190			}191		}192	}193}194195#[derive(Debug, Clone, Trace)]196pub enum StrValue {197	Flat(IStr),198	Tree(Rc<(StrValue, StrValue, usize)>),199}200impl StrValue {201	pub fn concat(a: StrValue, b: StrValue) -> Self {202		if a.is_empty() {203			b204		} else if b.is_empty() {205			a206		} else {207			let len = a.len() + b.len();208			Self::Tree(Rc::new((a, b, len)))209		}210	}211	pub fn into_flat(self) -> IStr {212		match self {213			StrValue::Flat(f) => f,214			StrValue::Tree(_) => {215				let mut buf = String::new();216				self.into_flat_buf(&mut buf);217				buf.into()218			}219		}220	}221	fn into_flat_buf(&self, out: &mut String) {222		match self {223			StrValue::Flat(f) => out.push_str(f),224			StrValue::Tree(t) => {225				t.0.into_flat_buf(out);226				t.1.into_flat_buf(out);227			}228		}229	}230	pub fn len(&self) -> usize {231		match self {232			StrValue::Flat(v) => v.len(),233			StrValue::Tree(t) => t.2,234		}235	}236	pub fn is_empty(&self) -> bool {237		match self {238			Self::Flat(v) => v.is_empty(),239			_ => false,240		}241	}242}243impl Display for StrValue {244	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {245		match self {246			StrValue::Flat(v) => write!(f, "{v}"),247			StrValue::Tree(t) => {248				write!(f, "{}", t.0)?;249				write!(f, "{}", t.1)250			}251		}252	}253}254impl PartialEq for StrValue {255	fn eq(&self, other: &Self) -> bool {256		let a = self.clone().into_flat();257		let b = other.clone().into_flat();258		a == b259	}260}261impl Eq for StrValue {}262impl PartialOrd for StrValue {263	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {264		let a = self.clone().into_flat();265		let b = other.clone().into_flat();266		Some(a.cmp(&b))267	}268}269impl Ord for StrValue {270	fn cmp(&self, other: &Self) -> std::cmp::Ordering {271		self.partial_cmp(other)272			.expect("partial_cmp always returns Some")273	}274}275276/// Represents any valid Jsonnet value.277#[derive(Debug, Clone, Trace)]278pub enum Val {279	/// Represents a Jsonnet boolean.280	Bool(bool),281	/// Represents a Jsonnet null value.282	Null,283	/// Represents a Jsonnet string.284	Str(StrValue),285	/// Represents a Jsonnet number.286	/// Should be finite, and not NaN287	/// This restriction isn't enforced by enum, as enum field can't be marked as private288	Num(f64),289	/// Represents a Jsonnet array.290	Arr(ArrValue),291	/// Represents a Jsonnet object.292	Obj(ObjValue),293	/// Represents a Jsonnet function.294	Func(FuncVal),295}296297static_assertions::assert_eq_size!(Val, [u8; 24]);298299impl From<IndexableVal> for Val {300	fn from(v: IndexableVal) -> Self {301		match v {302			IndexableVal::Str(s) => Self::Str(StrValue::Flat(s)),303			IndexableVal::Arr(a) => Self::Arr(a),304		}305	}306}307308impl Val {309	pub const fn as_bool(&self) -> Option<bool> {310		match self {311			Self::Bool(v) => Some(*v),312			_ => None,313		}314	}315	pub const fn as_null(&self) -> Option<()> {316		match self {317			Self::Null => Some(()),318			_ => None,319		}320	}321	pub fn as_str(&self) -> Option<IStr> {322		match self {323			Self::Str(s) => Some(s.clone().into_flat()),324			_ => None,325		}326	}327	pub const fn as_num(&self) -> Option<f64> {328		match self {329			Self::Num(n) => Some(*n),330			_ => None,331		}332	}333	pub fn as_arr(&self) -> Option<ArrValue> {334		match self {335			Self::Arr(a) => Some(a.clone()),336			_ => None,337		}338	}339	pub fn as_obj(&self) -> Option<ObjValue> {340		match self {341			Self::Obj(o) => Some(o.clone()),342			_ => None,343		}344	}345	pub fn as_func(&self) -> Option<FuncVal> {346		match self {347			Self::Func(f) => Some(f.clone()),348			_ => None,349		}350	}351352	/// Creates `Val::Num` after checking for numeric overflow.353	/// As numbers are `f64`, we can just check for their finity.354	pub fn new_checked_num(num: f64) -> Result<Self> {355		if num.is_finite() {356			Ok(Self::Num(num))357		} else {358			throw!("overflow")359		}360	}361362	pub const fn value_type(&self) -> ValType {363		match self {364			Self::Str(..) => ValType::Str,365			Self::Num(..) => ValType::Num,366			Self::Arr(..) => ValType::Arr,367			Self::Obj(..) => ValType::Obj,368			Self::Bool(_) => ValType::Bool,369			Self::Null => ValType::Null,370			Self::Func(..) => ValType::Func,371		}372	}373374	pub fn manifest(&self, format: impl ManifestFormat) -> Result<String> {375		fn manifest_dyn(val: &Val, manifest: &dyn ManifestFormat) -> Result<String> {376			manifest.manifest(val.clone())377		}378		manifest_dyn(self, &format)379	}380381	pub fn to_string(&self) -> Result<IStr> {382		Ok(match self {383			Self::Bool(true) => "true".into(),384			Self::Bool(false) => "false".into(),385			Self::Null => "null".into(),386			Self::Str(s) => s.clone().into_flat(),387			_ => self.manifest(ToStringFormat).map(IStr::from)?,388		})389	}390391	pub fn into_indexable(self) -> Result<IndexableVal> {392		Ok(match self {393			Val::Str(s) => IndexableVal::Str(s.into_flat()),394			Val::Arr(arr) => IndexableVal::Arr(arr),395			_ => throw!(ValueIsNotIndexable(self.value_type())),396		})397	}398}399400const fn is_function_like(val: &Val) -> bool {401	matches!(val, Val::Func(_))402}403404/// Native implementation of `std.primitiveEquals`405pub fn primitive_equals(val_a: &Val, val_b: &Val) -> Result<bool> {406	Ok(match (val_a, val_b) {407		(Val::Bool(a), Val::Bool(b)) => a == b,408		(Val::Null, Val::Null) => true,409		(Val::Str(a), Val::Str(b)) => a == b,410		(Val::Num(a), Val::Num(b)) => (a - b).abs() <= f64::EPSILON,411		(Val::Arr(_), Val::Arr(_)) => {412			throw!("primitiveEquals operates on primitive types, got array")413		}414		(Val::Obj(_), Val::Obj(_)) => {415			throw!("primitiveEquals operates on primitive types, got object")416		}417		(a, b) if is_function_like(a) && is_function_like(b) => {418			throw!("cannot test equality of functions")419		}420		(_, _) => false,421	})422}423424/// Native implementation of `std.equals`425pub fn equals(val_a: &Val, val_b: &Val) -> Result<bool> {426	if val_a.value_type() != val_b.value_type() {427		return Ok(false);428	}429	match (val_a, val_b) {430		(Val::Arr(a), Val::Arr(b)) => {431			if ArrValue::ptr_eq(a, b) {432				return Ok(true);433			}434			if a.len() != b.len() {435				return Ok(false);436			}437			for (a, b) in a.iter().zip(b.iter()) {438				if !equals(&a?, &b?)? {439					return Ok(false);440				}441			}442			Ok(true)443		}444		(Val::Obj(a), Val::Obj(b)) => {445			if ObjValue::ptr_eq(a, b) {446				return Ok(true);447			}448			let fields = a.fields(449				#[cfg(feature = "exp-preserve-order")]450				false,451			);452			if fields453				!= b.fields(454					#[cfg(feature = "exp-preserve-order")]455					false,456				) {457				return Ok(false);458			}459			for field in fields {460				if !equals(461					&a.get(field.clone())?.expect("field exists"),462					&b.get(field)?.expect("field exists"),463				)? {464					return Ok(false);465				}466			}467			Ok(true)468		}469		(a, b) => Ok(primitive_equals(a, b)?),470	}471}
modifiedcrates/jrsonnet-stdlib/src/arrays.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/arrays.rs
+++ b/crates/jrsonnet-stdlib/src/arrays.rs
@@ -37,12 +37,13 @@
 	func: NativeFn<((Either![String, Any],), Any)>,
 	arr: IndexableVal,
 ) -> Result<IndexableVal> {
+	use std::fmt::Write;
 	match arr {
 		IndexableVal::Str(str) => {
 			let mut out = String::new();
 			for c in str.chars() {
 				match func(Either2::A(c.to_string()))?.0 {
-					Val::Str(o) => out.push_str(&o),
+					Val::Str(o) => write!(out, "{o}").unwrap(),
 					Val::Null => continue,
 					_ => throw!("in std.join all items should be strings"),
 				};
@@ -101,6 +102,7 @@
 
 #[builtin]
 pub fn builtin_join(sep: IndexableVal, arr: ArrValue) -> Result<IndexableVal> {
+	use std::fmt::Write;
 	Ok(match sep {
 		IndexableVal::Arr(joiner_items) => {
 			let mut out = Vec::new();
@@ -141,7 +143,7 @@
 						out += &sep;
 					}
 					first = false;
-					out += &item;
+					write!(out, "{item}").unwrap()
 				} else if matches!(item, Val::Null) {
 					continue;
 				} else {
modifiedcrates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/lib.rs
+++ b/crates/jrsonnet-stdlib/src/lib.rs
@@ -320,15 +320,19 @@
 	}
 	#[cfg(feature = "legacy-this-file")]
 	fn initialize(&self, s: State, source: Source) -> Context {
+		use jrsonnet_evaluator::val::StrValue;
+
 		let mut builder = ObjValueBuilder::new();
 		builder.with_super(self.stdlib_obj.clone());
 		builder
 			.member("thisFile".into())
 			.hide()
-			.value(Val::Str(match source.source_path().path() {
-				Some(p) => self.settings().path_resolver.resolve(p).into(),
-				None => source.source_path().to_string().into(),
-			}))
+			.value(Val::Str(StrValue::Flat(
+				match source.source_path().path() {
+					Some(p) => self.settings().path_resolver.resolve(p).into(),
+					None => source.source_path().to_string().into(),
+				},
+			)))
 			.expect("this object builder is empty");
 		let stdlib_with_this_file = builder.build();
 
modifiedcrates/jrsonnet-stdlib/src/manifest/yaml.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/manifest/yaml.rs
+++ b/crates/jrsonnet-stdlib/src/manifest/yaml.rs
@@ -118,6 +118,7 @@
 		}
 		Val::Null => buf.push_str("null"),
 		Val::Str(s) => {
+			let s = s.clone().into_flat();
 			if s.is_empty() {
 				buf.push_str("\"\"");
 			} else if let Some(s) = s.strip_suffix('\n') {
@@ -128,10 +129,10 @@
 					buf.push_str(&options.padding);
 					buf.push_str(line);
 				}
-			} else if !options.quote_keys && !yaml_needs_quotes(s) {
-				buf.push_str(s);
+			} else if !options.quote_keys && !yaml_needs_quotes(&s) {
+				buf.push_str(&s);
 			} else {
-				escape_string_json_buf(s, buf);
+				escape_string_json_buf(&s, buf);
 			}
 		}
 		Val::Num(n) => write!(buf, "{}", *n).unwrap(),
modifiedcrates/jrsonnet-stdlib/src/objects.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/objects.rs
+++ b/crates/jrsonnet-stdlib/src/objects.rs
@@ -1,5 +1,9 @@
 use jrsonnet_evaluator::{
-	error::Result, function::builtin, typed::VecVal, val::Val, IStr, ObjValue,
+	error::Result,
+	function::builtin,
+	typed::VecVal,
+	val::{StrValue, Val},
+	IStr, ObjValue,
 };
 use jrsonnet_gcmodule::Cc;
 
@@ -17,7 +21,10 @@
 		preserve_order,
 	);
 	Ok(VecVal(Cc::new(
-		out.into_iter().map(Val::Str).collect::<Vec<_>>(),
+		out.into_iter()
+			.map(StrValue::Flat)
+			.map(Val::Str)
+			.collect::<Vec<_>>(),
 	)))
 }
 
modifiedcrates/jrsonnet-stdlib/src/operator.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/operator.rs
+++ b/crates/jrsonnet-stdlib/src/operator.rs
@@ -7,7 +7,7 @@
 	operator::evaluate_mod_op,
 	stdlib::std_format,
 	typed::{Any, Either, Either2},
-	val::{equals, primitive_equals},
+	val::{equals, primitive_equals, StrValue},
 	IStr, Val,
 };
 
@@ -17,7 +17,7 @@
 	Ok(Any(evaluate_mod_op(
 		&match a {
 			A(v) => Val::Num(v),
-			B(s) => Val::Str(s),
+			B(s) => Val::Str(StrValue::Flat(s)),
 		},
 		&b.0,
 	)?))
@@ -35,5 +35,5 @@
 
 #[builtin]
 pub fn builtin_format(str: IStr, vals: Any) -> Result<String> {
-	std_format(str, vals.0)
+	std_format(&str, vals.0)
 }
modifiedcrates/jrsonnet-stdlib/src/strings.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/strings.rs
+++ b/crates/jrsonnet-stdlib/src/strings.rs
@@ -3,7 +3,7 @@
 	function::builtin,
 	throw,
 	typed::{Either2, VecVal, M1},
-	val::ArrValue,
+	val::{ArrValue, StrValue},
 	Either, IStr, Val,
 };
 use jrsonnet_gcmodule::Cc;
@@ -34,9 +34,12 @@
 	Ok(VecVal(Cc::new(match maxsplits {
 		A(n) => str
 			.splitn(n + 1, &c as &str)
-			.map(|s| Val::Str(s.into()))
+			.map(|s| Val::Str(StrValue::Flat(s.into())))
+			.collect(),
+		B(_) => str
+			.split(&c as &str)
+			.map(|s| Val::Str(StrValue::Flat(s.into())))
 			.collect(),
-		B(_) => str.split(&c as &str).map(|s| Val::Str(s.into())).collect(),
 	})))
 }