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
before · crates/jrsonnet-evaluator/src/manifest.rs
1use std::{borrow::Cow, fmt::Write};23use crate::{4	error::{ErrorKind::*, Result},5	throw, State, Val,6};78pub trait ManifestFormat {9	fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()>;10	fn manifest(&self, val: Val) -> Result<String> {11		let mut out = String::new();12		self.manifest_buf(val, &mut out)?;13		Ok(out)14	}15}16impl<T> ManifestFormat for Box<T>17where18	T: ManifestFormat + ?Sized,19{20	fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {21		let inner = &**self;22		inner.manifest_buf(val, buf)23	}24}25impl<T> ManifestFormat for &'_ T26where27	T: ManifestFormat + ?Sized,28{29	fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {30		let inner = &**self;31		inner.manifest_buf(val, buf)32	}33}3435#[derive(PartialEq, Eq, Clone, Copy)]36enum JsonFormatting {37	// Applied in manifestification38	Manifest,39	/// Used for std.manifestJson40	/// Empty array/objects extends to "[\n\n]" instead of "[ ]" as in manifest41	Std,42	/// No line breaks, used in `obj+''`43	ToString,44	/// Minified json45	Minify,46}4748pub struct JsonFormat<'s> {49	padding: Cow<'s, str>,50	mtype: JsonFormatting,51	newline: &'s str,52	key_val_sep: &'s str,53	#[cfg(feature = "exp-preserve-order")]54	preserve_order: bool,55}5657impl<'s> JsonFormat<'s> {58	// Minifying format59	pub fn minify(#[cfg(feature = "exp-preserve-order")] preserve_order: bool) -> Self {60		Self {61			padding: Cow::Borrowed(""),62			mtype: JsonFormatting::Minify,63			newline: "\n",64			key_val_sep: ":",65			#[cfg(feature = "exp-preserve-order")]66			preserve_order,67		}68	}69	// Same format as std.toString70	pub fn std_to_string() -> Self {71		Self {72			padding: Cow::Borrowed(""),73			mtype: JsonFormatting::ToString,74			newline: "\n",75			key_val_sep: ": ",76			#[cfg(feature = "exp-preserve-order")]77			preserve_order: false,78		}79	}80	pub fn std_to_json(81		padding: String,82		newline: &'s str,83		key_val_sep: &'s str,84		#[cfg(feature = "exp-preserve-order")] preserve_order: bool,85	) -> Self {86		Self {87			padding: Cow::Owned(padding),88			mtype: JsonFormatting::Std,89			newline,90			key_val_sep,91			#[cfg(feature = "exp-preserve-order")]92			preserve_order,93		}94	}95	// Same format as CLI manifestification96	pub fn cli(97		padding: usize,98		#[cfg(feature = "exp-preserve-order")] preserve_order: bool,99	) -> Self {100		if padding == 0 {101			return Self::minify(102				#[cfg(feature = "exp-preserve-order")]103				preserve_order,104			);105		}106		Self {107			padding: Cow::Owned(" ".repeat(padding)),108			mtype: JsonFormatting::Manifest,109			newline: "\n",110			key_val_sep: ": ",111			#[cfg(feature = "exp-preserve-order")]112			preserve_order,113		}114	}115}116impl Default for JsonFormat<'static> {117	fn default() -> Self {118		Self {119			padding: Cow::Borrowed("    "),120			mtype: JsonFormatting::Manifest,121			newline: "\n",122			key_val_sep: ": ",123			#[cfg(feature = "exp-preserve-order")]124			preserve_order: false,125		}126	}127}128129pub fn manifest_json_ex(val: &Val, options: &JsonFormat<'_>) -> Result<String> {130	let mut out = String::new();131	manifest_json_ex_buf(val, &mut out, &mut String::new(), options)?;132	Ok(out)133}134fn manifest_json_ex_buf(135	val: &Val,136	buf: &mut String,137	cur_padding: &mut String,138	options: &JsonFormat<'_>,139) -> Result<()> {140	let mtype = options.mtype;141	match val {142		Val::Bool(v) => {143			if *v {144				buf.push_str("true");145			} else {146				buf.push_str("false");147			}148		}149		Val::Null => buf.push_str("null"),150		Val::Str(s) => escape_string_json_buf(s, buf),151		Val::Num(n) => write!(buf, "{n}").unwrap(),152		Val::Arr(items) => {153			buf.push('[');154			if !items.is_empty() {155				if mtype != JsonFormatting::ToString && mtype != JsonFormatting::Minify {156					buf.push_str(options.newline);157				}158159				let old_len = cur_padding.len();160				cur_padding.push_str(&options.padding);161				for (i, item) in items.iter().enumerate() {162					if i != 0 {163						buf.push(',');164						if mtype == JsonFormatting::ToString {165							buf.push(' ');166						} else if mtype != JsonFormatting::Minify {167							buf.push_str(options.newline);168						}169					}170					buf.push_str(cur_padding);171					manifest_json_ex_buf(&item?, buf, cur_padding, options)?;172				}173				cur_padding.truncate(old_len);174175				if mtype != JsonFormatting::ToString && mtype != JsonFormatting::Minify {176					buf.push_str(options.newline);177					buf.push_str(cur_padding);178				}179			} else if mtype == JsonFormatting::Std {180				buf.push_str("\n\n");181				buf.push_str(cur_padding);182			} else if mtype == JsonFormatting::ToString || mtype == JsonFormatting::Manifest {183				buf.push(' ');184			}185			buf.push(']');186		}187		Val::Obj(obj) => {188			obj.run_assertions()?;189			buf.push('{');190			let fields = obj.fields(191				#[cfg(feature = "exp-preserve-order")]192				options.preserve_order,193			);194			if !fields.is_empty() {195				if mtype != JsonFormatting::ToString && mtype != JsonFormatting::Minify {196					buf.push_str(options.newline);197				}198199				let old_len = cur_padding.len();200				cur_padding.push_str(&options.padding);201				for (i, field) in fields.into_iter().enumerate() {202					if i != 0 {203						buf.push(',');204						if mtype == JsonFormatting::ToString {205							buf.push(' ');206						} else if mtype != JsonFormatting::Minify {207							buf.push_str(options.newline);208						}209					}210					buf.push_str(cur_padding);211					escape_string_json_buf(&field, buf);212					buf.push_str(options.key_val_sep);213					State::push_description(214						|| format!("field <{}> manifestification", field.clone()),215						|| {216							let value = obj.get(field.clone())?.unwrap();217							manifest_json_ex_buf(&value, buf, cur_padding, options)?;218							Ok(())219						},220					)?;221				}222				cur_padding.truncate(old_len);223224				if mtype != JsonFormatting::ToString && mtype != JsonFormatting::Minify {225					buf.push_str(options.newline);226					buf.push_str(cur_padding);227				}228			} else if mtype == JsonFormatting::Std {229				buf.push_str("\n\n");230				buf.push_str(cur_padding);231			} else if mtype == JsonFormatting::ToString || mtype == JsonFormatting::Manifest {232				buf.push(' ');233			}234			buf.push('}');235		}236		Val::Func(_) => throw!(RuntimeError("tried to manifest function".into())),237	};238	Ok(())239}240241impl ManifestFormat for JsonFormat<'_> {242	fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {243		manifest_json_ex_buf(&val, buf, &mut String::new(), self)244	}245}246247pub struct ToStringFormat;248impl ManifestFormat for ToStringFormat {249	fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {250		JsonFormat::std_to_string().manifest_buf(val, out)251	}252}253pub struct StringFormat;254impl ManifestFormat for StringFormat {255	fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {256		let Val::Str(s) = val else {257			throw!("output should be string for string manifest format, got {}", val.value_type())258		};259		out.write_str(&s).unwrap();260		Ok(())261	}262}263264pub struct YamlStreamFormat<I>(pub I);265impl<I: ManifestFormat> ManifestFormat for YamlStreamFormat<I> {266	fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {267		let Val::Arr(arr) = val else {268			throw!("output should be array for yaml stream format, got {}", val.value_type())269		};270		if !arr.is_empty() {271			for v in arr.iter() {272				let v = v?;273				out.push_str("---\n");274				self.0.manifest_buf(v, out)?;275				out.push('\n');276			}277			out.push_str("...");278		}279		Ok(())280	}281}282283pub fn escape_string_json(s: &str) -> String {284	let mut buf = String::new();285	escape_string_json_buf(s, &mut buf);286	buf287}288289// Json string encoding was borrowed from https://github.com/serde-rs/json290291const BB: u8 = b'b'; // \x08292const TT: u8 = b't'; // \x09293const NN: u8 = b'n'; // \x0A294const FF: u8 = b'f'; // \x0C295const RR: u8 = b'r'; // \x0D296const QU: u8 = b'"'; // \x22297const BS: u8 = b'\\'; // \x5C298const UU: u8 = b'u'; // \x00...\x1F except the ones above299const __: u8 = 0;300301// Lookup table of escape sequences. A value of b'x' at index i means that byte302// i is escaped as "\x" in JSON. A value of 0 means that byte i is not escaped.303static ESCAPE: [u8; 256] = [304	//   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F305	UU, UU, UU, UU, UU, UU, UU, UU, BB, TT, NN, UU, FF, RR, UU, UU, // 0306	UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, // 1307	__, __, QU, __, __, __, __, __, __, __, __, __, __, __, __, __, // 2308	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 3309	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 4310	__, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, // 5311	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 6312	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 7313	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 8314	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 9315	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // A316	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // B317	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // C318	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // D319	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // E320	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // F321];322323pub fn escape_string_json_buf(value: &str, buf: &mut String) {324	// Safety: we only write correct utf-8 in this function325	let buf: &mut Vec<u8> = unsafe { &mut *(buf as *mut String).cast::<Vec<u8>>() };326	let bytes = value.as_bytes();327328	// Perfect for ascii strings, removes any reallocations329	buf.reserve(value.len() + 2);330331	buf.push(b'"');332333	let mut start = 0;334335	for (i, &byte) in bytes.iter().enumerate() {336		let escape = ESCAPE[byte as usize];337		if escape == __ {338			continue;339		}340341		if start < i {342			buf.extend_from_slice(&bytes[start..i]);343		}344		start = i + 1;345346		match escape {347			self::BB | self::TT | self::NN | self::FF | self::RR | self::QU | self::BS => {348				buf.extend_from_slice(&[b'\\', escape]);349			}350			self::UU => {351				static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";352				let bytes = &[353					b'\\',354					b'u',355					b'0',356					b'0',357					HEX_DIGITS[(byte >> 4) as usize],358					HEX_DIGITS[(byte & 0xF) as usize],359				];360				buf.extend_from_slice(bytes);361			}362			_ => unreachable!(),363		}364	}365366	if start == bytes.len() {367		buf.push(b'"');368		return;369	}370371	buf.extend_from_slice(&bytes[start..]);372	buf.push(b'"');373}
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
--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -1,4 +1,9 @@
-use std::{cell::RefCell, fmt::Debug, mem::replace};
+use std::{
+	cell::RefCell,
+	fmt::{self, Debug, Display},
+	mem::replace,
+	rc::Rc,
+};
 
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
@@ -117,7 +122,7 @@
 }
 
 impl<T: Debug + Trace> Debug for Thunk<T> {
-	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 		write!(f, "Lazy")
 	}
 }
@@ -187,6 +192,87 @@
 	}
 }
 
+#[derive(Debug, Clone, Trace)]
+pub enum StrValue {
+	Flat(IStr),
+	Tree(Rc<(StrValue, StrValue, usize)>),
+}
+impl StrValue {
+	pub fn concat(a: StrValue, b: StrValue) -> Self {
+		if a.is_empty() {
+			b
+		} else if b.is_empty() {
+			a
+		} else {
+			let len = a.len() + b.len();
+			Self::Tree(Rc::new((a, b, len)))
+		}
+	}
+	pub fn into_flat(self) -> IStr {
+		match self {
+			StrValue::Flat(f) => f,
+			StrValue::Tree(_) => {
+				let mut buf = String::new();
+				self.into_flat_buf(&mut buf);
+				buf.into()
+			}
+		}
+	}
+	fn into_flat_buf(&self, out: &mut String) {
+		match self {
+			StrValue::Flat(f) => out.push_str(f),
+			StrValue::Tree(t) => {
+				t.0.into_flat_buf(out);
+				t.1.into_flat_buf(out);
+			}
+		}
+	}
+	pub fn len(&self) -> usize {
+		match self {
+			StrValue::Flat(v) => v.len(),
+			StrValue::Tree(t) => t.2,
+		}
+	}
+	pub fn is_empty(&self) -> bool {
+		match self {
+			Self::Flat(v) => v.is_empty(),
+			_ => false,
+		}
+	}
+}
+impl Display for StrValue {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		match self {
+			StrValue::Flat(v) => write!(f, "{v}"),
+			StrValue::Tree(t) => {
+				write!(f, "{}", t.0)?;
+				write!(f, "{}", t.1)
+			}
+		}
+	}
+}
+impl PartialEq for StrValue {
+	fn eq(&self, other: &Self) -> bool {
+		let a = self.clone().into_flat();
+		let b = other.clone().into_flat();
+		a == b
+	}
+}
+impl Eq for StrValue {}
+impl PartialOrd for StrValue {
+	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+		let a = self.clone().into_flat();
+		let b = other.clone().into_flat();
+		Some(a.cmp(&b))
+	}
+}
+impl Ord for StrValue {
+	fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+		self.partial_cmp(other)
+			.expect("partial_cmp always returns Some")
+	}
+}
+
 /// Represents any valid Jsonnet value.
 #[derive(Debug, Clone, Trace)]
 pub enum Val {
@@ -195,7 +281,7 @@
 	/// Represents a Jsonnet null value.
 	Null,
 	/// Represents a Jsonnet string.
-	Str(IStr),
+	Str(StrValue),
 	/// Represents a Jsonnet number.
 	/// Should be finite, and not NaN
 	/// This restriction isn't enforced by enum, as enum field can't be marked as private
@@ -208,10 +294,12 @@
 	Func(FuncVal),
 }
 
+static_assertions::assert_eq_size!(Val, [u8; 24]);
+
 impl From<IndexableVal> for Val {
 	fn from(v: IndexableVal) -> Self {
 		match v {
-			IndexableVal::Str(s) => Self::Str(s),
+			IndexableVal::Str(s) => Self::Str(StrValue::Flat(s)),
 			IndexableVal::Arr(a) => Self::Arr(a),
 		}
 	}
@@ -232,7 +320,7 @@
 	}
 	pub fn as_str(&self) -> Option<IStr> {
 		match self {
-			Self::Str(s) => Some(s.clone()),
+			Self::Str(s) => Some(s.clone().into_flat()),
 			_ => None,
 		}
 	}
@@ -295,14 +383,14 @@
 			Self::Bool(true) => "true".into(),
 			Self::Bool(false) => "false".into(),
 			Self::Null => "null".into(),
-			Self::Str(s) => s.clone(),
+			Self::Str(s) => s.clone().into_flat(),
 			_ => self.manifest(ToStringFormat).map(IStr::from)?,
 		})
 	}
 
 	pub fn into_indexable(self) -> Result<IndexableVal> {
 		Ok(match self {
-			Val::Str(s) => IndexableVal::Str(s),
+			Val::Str(s) => IndexableVal::Str(s.into_flat()),
 			Val::Arr(arr) => IndexableVal::Arr(arr),
 			_ => throw!(ValueIsNotIndexable(self.value_type())),
 		})
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(),
 	})))
 }