--- 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::() .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!(), } --- 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 { @@ -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), --- 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> { 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 { --- 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(deserializer: D) -> Result @@ -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()))?; --- 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(()) } } --- 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(),); } --- 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 { +pub fn std_format(str: &str, vals: Val) -> Result { State::push( CallLocation::native(), || format!("std.format of {str}"), --- 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 { - Ok(Val::Str(value)) + Ok(Val::Str(StrValue::Flat(value))) } fn from_untyped(value: Val) -> Result { ::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 { - Ok(Val::Str(value.into())) + Ok(Val::Str(StrValue::Flat(value.into()))) } fn from_untyped(value: Val) -> Result { @@ -219,13 +219,13 @@ const TYPE: &'static ComplexValType = &ComplexValType::Char; fn into_untyped(value: Self) -> Result { - Ok(Val::Str(value.to_string().into())) + Ok(Val::Str(StrValue::Flat(value.to_string().into()))) } fn from_untyped(value: Val) -> Result { ::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 { 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)), } } --- 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) => { --- 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 Debug for Thunk { - 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 { + 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 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 { 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 { 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())), }) --- 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 { + 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 { + 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 { --- 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(); --- 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(), --- 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::>(), + out.into_iter() + .map(StrValue::Flat) + .map(Val::Str) + .collect::>(), ))) } --- 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 { - std_format(str, vals.0) + std_format(&str, vals.0) } --- 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(), }))) }