diff --git a/src/eco.rs b/src/eco.rs new file mode 100644 index 000000000..2d2ab2dc0 --- /dev/null +++ b/src/eco.rs @@ -0,0 +1,389 @@ +//! An economical string. + +use std::borrow::Borrow; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Display, Formatter, Write}; +use std::hash::{Hash, Hasher}; +use std::ops::{Add, AddAssign, Deref}; +use std::rc::Rc; + +/// A economical string with inline storage and clone-on-write value semantics. +#[derive(Clone)] +pub struct EcoString(Repr); + +/// The internal representation. Either: +/// - inline when below a certain number of bytes, +/// - or reference-counted on the heap with COW semantics. +#[derive(Clone)] +enum Repr { + Small { buf: [u8; LIMIT], len: u8 }, + Large(Rc), +} + +/// The maximum number of bytes that can be stored inline. +/// +/// The value is chosen such that `Repr` fits exactly into 16 bytes +/// (which are needed anyway due to `Rc`s alignment). +/// +/// Must be at least 4 to hold any char. +const LIMIT: usize = 14; + +impl EcoString { + /// Create a new, empty string. + pub fn new() -> Self { + Self(Repr::Small { buf: [0; LIMIT], len: 0 }) + } + + /// Create a new, empty string with the given `capacity`. + pub fn with_capacity(capacity: usize) -> Self { + if capacity <= LIMIT { + Self::new() + } else { + Self(Repr::Large(Rc::new(String::with_capacity(capacity)))) + } + } + + /// Create an instance from an existing string-like type. + pub fn from_str(s: S) -> Self + where + S: AsRef + Into, + { + let slice = s.as_ref(); + let len = slice.len(); + Self(if len <= LIMIT { + let mut buf = [0; LIMIT]; + buf[.. len].copy_from_slice(slice.as_bytes()); + Repr::Small { buf, len: len as u8 } + } else { + Repr::Large(Rc::new(s.into())) + }) + } + + /// Whether the string is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// The length of the string in bytes. + pub fn len(&self) -> usize { + match &self.0 { + Repr::Small { len, .. } => usize::from(*len), + Repr::Large(string) => string.len(), + } + } + + /// A string slice containing the entire string. + pub fn as_str(&self) -> &str { + self + } + + /// Appends the given character at the end. + pub fn push(&mut self, c: char) { + match &mut self.0 { + Repr::Small { buf, len } => { + let prev = usize::from(*len); + if c.len_utf8() == 1 && prev < LIMIT { + buf[prev] = c as u8; + *len += 1; + } else { + self.push_str(c.encode_utf8(&mut [0; 4])); + } + } + Repr::Large(rc) => Rc::make_mut(rc).push(c), + } + } + + /// Appends the given string slice at the end. + pub fn push_str(&mut self, string: &str) { + match &mut self.0 { + Repr::Small { buf, len } => { + let prev = usize::from(*len); + let new = prev + string.len(); + if new <= LIMIT { + buf[prev .. new].copy_from_slice(string.as_bytes()); + *len = new as u8; + } else { + let mut spilled = String::with_capacity(new); + spilled.push_str(self); + spilled.push_str(string); + self.0 = Repr::Large(Rc::new(spilled)); + } + } + Repr::Large(rc) => Rc::make_mut(rc).push_str(string), + } + } + + /// Removes the last character from the string. + pub fn pop(&mut self) -> Option { + let c = self.as_str().chars().rev().next()?; + match &mut self.0 { + Repr::Small { len, .. } => { + *len -= c.len_utf8() as u8; + } + Repr::Large(rc) => { + Rc::make_mut(rc).pop(); + } + } + Some(c) + } + + /// Repeats this string `n` times. + pub fn repeat(&self, n: usize) -> Self { + if let Repr::Small { buf, len } = &self.0 { + let prev = usize::from(*len); + let new = prev.saturating_mul(n); + if new <= LIMIT { + let src = &buf[.. prev]; + let mut buf = [0; LIMIT]; + for i in 0 .. n { + buf[prev * i .. prev * (i + 1)].copy_from_slice(src); + } + return Self(Repr::Small { buf, len: new as u8 }); + } + } + + self.as_str().repeat(n).into() + } +} + +impl From<&Self> for EcoString { + fn from(s: &Self) -> Self { + s.clone() + } +} + +impl From for EcoString { + fn from(c: char) -> Self { + let mut buf = [0; LIMIT]; + let len = c.encode_utf8(&mut buf).len(); + Self(Repr::Small { buf, len: len as u8 }) + } +} + +impl From<&str> for EcoString { + fn from(s: &str) -> Self { + Self::from_str(s) + } +} + +impl From for EcoString { + fn from(s: String) -> Self { + Self::from_str(s) + } +} + +impl From<&String> for EcoString { + fn from(s: &String) -> Self { + Self::from_str(s) + } +} + +impl Deref for EcoString { + type Target = str; + + fn deref(&self) -> &str { + match &self.0 { + Repr::Small { buf, len } => unsafe { + std::str::from_utf8_unchecked(&buf[.. usize::from(*len)]) + }, + Repr::Large(string) => string.as_str(), + } + } +} + +impl AsRef for EcoString { + fn as_ref(&self) -> &str { + self + } +} + +impl Borrow for EcoString { + fn borrow(&self) -> &str { + self + } +} + +impl Default for EcoString { + fn default() -> Self { + Self::new() + } +} + +impl Debug for EcoString { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(self.as_str(), f) + } +} + +impl Display for EcoString { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(self.as_str(), f) + } +} + +impl Eq for EcoString {} + +impl PartialEq for EcoString { + fn eq(&self, other: &Self) -> bool { + self.as_str().eq(other.as_str()) + } +} + +impl PartialEq for EcoString { + fn eq(&self, other: &str) -> bool { + self.as_str().eq(other) + } +} + +impl PartialEq<&str> for EcoString { + fn eq(&self, other: &&str) -> bool { + self.as_str().eq(*other) + } +} + +impl PartialEq for EcoString { + fn eq(&self, other: &String) -> bool { + self.as_str().eq(other.as_str()) + } +} + +impl Ord for EcoString { + fn cmp(&self, other: &Self) -> Ordering { + self.as_str().cmp(other.as_str()) + } +} + +impl PartialOrd for EcoString { + fn partial_cmp(&self, other: &Self) -> Option { + self.as_str().partial_cmp(other.as_str()) + } +} + +impl Add<&str> for EcoString { + type Output = Self; + + fn add(mut self, rhs: &str) -> Self::Output { + self.push_str(rhs); + self + } +} + +impl AddAssign<&str> for EcoString { + fn add_assign(&mut self, rhs: &str) { + self.push_str(rhs); + } +} + +impl Hash for EcoString { + fn hash(&self, state: &mut H) { + self.as_str().hash(state); + } +} + +impl Write for EcoString { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.push_str(s); + Ok(()) + } + + fn write_char(&mut self, c: char) -> fmt::Result { + self.push(c); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const ALPH: &str = "abcdefghijklmnopqrstuvwxyz"; + + #[test] + fn test_str_new() { + // Test inline strings. + assert_eq!(EcoString::new(), ""); + assert_eq!(EcoString::from('a'), "a"); + assert_eq!(EcoString::from('😀'), "😀"); + assert_eq!(EcoString::from("abc"), "abc"); + + // Test around the inline limit. + assert_eq!(EcoString::from(&ALPH[.. LIMIT - 1]), ALPH[.. LIMIT - 1]); + assert_eq!(EcoString::from(&ALPH[.. LIMIT]), ALPH[.. LIMIT]); + assert_eq!(EcoString::from(&ALPH[.. LIMIT + 1]), ALPH[.. LIMIT + 1]); + + // Test heap string. + assert_eq!(EcoString::from(ALPH), ALPH); + } + + #[test] + fn test_str_push() { + let mut v = EcoString::new(); + v.push('a'); + v.push('b'); + v.push_str("cd😀"); + assert_eq!(v, "abcd😀"); + assert_eq!(v.len(), 8); + + // Test fully filling the inline storage. + v.push_str("efghij"); + assert_eq!(v.len(), LIMIT); + + // Test spilling with `push`. + let mut a = v.clone(); + a.push('k'); + assert_eq!(a, "abcd😀efghijk"); + assert_eq!(a.len(), 15); + + // Test spilling with `push_str`. + let mut b = v.clone(); + b.push_str("klmn"); + assert_eq!(b, "abcd😀efghijklmn"); + assert_eq!(b.len(), 18); + + // v should be unchanged. + assert_eq!(v.len(), LIMIT); + } + + #[test] + fn test_str_pop() { + // Test with inline string. + let mut v = EcoString::from("Hello World!"); + assert_eq!(v.pop(), Some('!')); + assert_eq!(v, "Hello World"); + + // Remove one-by-one. + for _ in 0 .. 10 { + v.pop(); + } + + assert_eq!(v, "H"); + assert_eq!(v.pop(), Some('H')); + assert_eq!(v, ""); + assert!(v.is_empty()); + + // Test with large string. + let mut v = EcoString::from(ALPH); + assert_eq!(v.pop(), Some('z')); + assert_eq!(v.len(), 25); + } + + #[test] + fn test_str_index() { + // Test that we can use the index syntax. + let v = EcoString::from("abc"); + assert_eq!(&v[.. 2], "ab"); + } + + #[test] + fn test_str_repeat() { + // Test with empty string. + assert_eq!(EcoString::new().repeat(0), ""); + assert_eq!(EcoString::new().repeat(100), ""); + + // Test non-spilling and spilling case. + let v = EcoString::from("abc"); + assert_eq!(v.repeat(0), ""); + assert_eq!(v.repeat(3), "abcabcabc"); + assert_eq!(v.repeat(5), "abcabcabcabcabc"); + } +} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 4992f70cc..dc4ab7ee6 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -17,6 +17,7 @@ use std::rc::Rc; use crate::cache::Cache; use crate::diag::{Diag, DiagSet, Pass}; +use crate::eco::EcoString; use crate::geom::{Angle, Fractional, Length, Relative}; use crate::loading::{FileHash, Loader}; use crate::parse::parse; @@ -528,7 +529,7 @@ impl Eval for ClosureExpr { visitor.finish() }; - let name = self.name.as_ref().map(|id| id.to_string()); + let name = self.name.as_ref().map(|name| name.string.clone()); Value::Func(FuncValue::new(name, move |ctx, args| { // Don't leak the scopes from the call site. Instead, we use the // scope of captured variables we collected earlier. @@ -555,7 +556,7 @@ impl Eval for WithExpr { let callee = self.callee.eval(ctx); if let Some(func) = ctx.cast::(callee, self.callee.span()) { let applied = self.args.eval(ctx); - let name = func.name().map(|s| s.to_string()); + let name = func.name().cloned(); Value::Func(FuncValue::new(name, move |ctx, args| { // Remove named arguments that were overridden. let kept: Vec<_> = applied @@ -698,7 +699,7 @@ impl Eval for ImportExpr { fn eval(&self, ctx: &mut EvalContext) -> Self::Output { let path = self.path.eval(ctx); - if let Some(path) = ctx.cast::(path, self.path.span()) { + if let Some(path) = ctx.cast::(path, self.path.span()) { if let Some(hash) = ctx.import(&path, self.path.span()) { let mut module = &ctx.modules[&hash]; match &self.imports { @@ -734,7 +735,7 @@ impl Eval for IncludeExpr { fn eval(&self, ctx: &mut EvalContext) -> Self::Output { let path = self.path.eval(ctx); - if let Some(path) = ctx.cast::(path, self.path.span()) { + if let Some(path) = ctx.cast::(path, self.path.span()) { if let Some(hash) = ctx.import(&path, self.path.span()) { return Value::Template(ctx.modules[&hash].template.clone()); } diff --git a/src/eval/scope.rs b/src/eval/scope.rs index 3f7c4c621..05bbeda24 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -4,7 +4,7 @@ use std::fmt::{self, Debug, Display, Formatter}; use std::iter; use std::rc::Rc; -use super::{AnyValue, EvalContext, FuncArgs, FuncValue, Type, Value}; +use super::{AnyValue, EcoString, EvalContext, FuncArgs, FuncValue, Type, Value}; /// A slot where a variable is stored. pub type Slot = Rc>; @@ -39,17 +39,17 @@ impl<'a> Scopes<'a> { } /// Define a constant variable with a value in the active scope. - pub fn def_const(&mut self, var: impl Into, value: impl Into) { + pub fn def_const(&mut self, var: impl Into, value: impl Into) { self.top.def_const(var, value); } /// Define a mutable variable with a value in the active scope. - pub fn def_mut(&mut self, var: impl Into, value: impl Into) { + pub fn def_mut(&mut self, var: impl Into, value: impl Into) { self.top.def_mut(var, value); } /// Define a variable with a slot in the active scope. - pub fn def_slot(&mut self, var: impl Into, slot: Slot) { + pub fn def_slot(&mut self, var: impl Into, slot: Slot) { self.top.def_slot(var, slot); } @@ -66,7 +66,7 @@ impl<'a> Scopes<'a> { #[derive(Default, Clone, PartialEq)] pub struct Scope { /// The mapping from names to slots. - values: HashMap, + values: HashMap, } impl Scope { @@ -76,7 +76,7 @@ impl Scope { } /// Define a constant variable with a value. - pub fn def_const(&mut self, var: impl Into, value: impl Into) { + pub fn def_const(&mut self, var: impl Into, value: impl Into) { let cell = RefCell::new(value.into()); // Make it impossible to write to this value again. @@ -87,7 +87,7 @@ impl Scope { } /// Define a constant function. - pub fn def_func(&mut self, name: impl Into, f: F) + pub fn def_func(&mut self, name: impl Into, f: F) where F: Fn(&mut EvalContext, &mut FuncArgs) -> Value + 'static, { @@ -96,7 +96,7 @@ impl Scope { } /// Define a constant variable with a value of variant `Value::Any`. - pub fn def_any(&mut self, var: impl Into, any: T) + pub fn def_any(&mut self, var: impl Into, any: T) where T: Type + Debug + Display + Clone + PartialEq + 'static, { @@ -104,12 +104,12 @@ impl Scope { } /// Define a mutable variable with a value. - pub fn def_mut(&mut self, var: impl Into, value: impl Into) { + pub fn def_mut(&mut self, var: impl Into, value: impl Into) { self.values.insert(var.into(), Rc::new(RefCell::new(value.into()))); } /// Define a variable with a slot. - pub fn def_slot(&mut self, var: impl Into, slot: Slot) { + pub fn def_slot(&mut self, var: impl Into, slot: Slot) { self.values.insert(var.into(), slot); } diff --git a/src/eval/value.rs b/src/eval/value.rs index 07552c845..2881399b6 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -5,8 +5,7 @@ use std::fmt::{self, Debug, Display, Formatter}; use std::ops::Deref; use std::rc::Rc; -use super::ops; -use super::EvalContext; +use super::*; use crate::color::{Color, RgbaColor}; use crate::exec::ExecContext; use crate::geom::{Angle, Fractional, Length, Linear, Relative}; @@ -38,7 +37,7 @@ pub enum Value { /// A color value: `#f79143ff`. Color(Color), /// A string: `"string"`. - Str(String), + Str(EcoString), /// An array value: `(1, "hi", 12cm)`. Array(ArrayValue), /// A dictionary value: `(color: #f79143, pattern: dashed)`. @@ -76,7 +75,7 @@ impl Value { Self::Linear(_) => Linear::TYPE_NAME, Self::Fractional(_) => Fractional::TYPE_NAME, Self::Color(_) => Color::TYPE_NAME, - Self::Str(_) => String::TYPE_NAME, + Self::Str(_) => EcoString::TYPE_NAME, Self::Array(_) => ArrayValue::TYPE_NAME, Self::Dict(_) => DictValue::TYPE_NAME, Self::Template(_) => TemplateValue::TYPE_NAME, @@ -151,7 +150,7 @@ impl Default for Value { pub type ArrayValue = Vec; /// A dictionary value: `(color: #f79143, pattern: dashed)`. -pub type DictValue = BTreeMap; +pub type DictValue = BTreeMap; /// A template value: `[*Hi* there]`. pub type TemplateValue = Rc>; @@ -171,7 +170,7 @@ pub enum TemplateNode { map: ExprMap, }, /// A template that was converted from a string. - Str(String), + Str(EcoString), /// A function template that can implement custom behaviour. Func(TemplateFunc), } @@ -224,14 +223,14 @@ impl Debug for TemplateFunc { pub struct FuncValue { /// The string is boxed to make the whole struct fit into 24 bytes, so that /// a [`Value`] fits into 32 bytes. - name: Option>, + name: Option>, /// The closure that defines the function. f: Rc Value>, } impl FuncValue { /// Create a new function value from a rust function or closure. - pub fn new(name: Option, f: F) -> Self + pub fn new(name: Option, f: F) -> Self where F: Fn(&mut EvalContext, &mut FuncArgs) -> Value + 'static, { @@ -239,8 +238,8 @@ impl FuncValue { } /// The name of the function. - pub fn name(&self) -> Option<&str> { - self.name.as_ref().map(|s| s.as_str()) + pub fn name(&self) -> Option<&EcoString> { + self.name.as_ref().map(|s| &**s) } } @@ -342,7 +341,7 @@ impl FuncArgs { let index = self .items .iter() - .position(|arg| arg.name.as_ref().map_or(false, |other| name == other))?; + .position(|arg| arg.name.as_ref().map_or(false, |other| other == name))?; let value = self.items.remove(index).value; let span = value.span; @@ -381,7 +380,7 @@ pub struct FuncArg { /// The span of the whole argument. pub span: Span, /// The name of the argument (`None` for positional arguments). - pub name: Option, + pub name: Option, /// The value of the argument. pub value: Spanned, } @@ -608,7 +607,7 @@ primitive! { } primitive! { Fractional: "fractional", Value::Fractional } primitive! { Color: "color", Value::Color } -primitive! { String: "string", Value::Str } +primitive! { EcoString: "string", Value::Str } primitive! { ArrayValue: "array", Value::Array } primitive! { DictValue: "dictionary", Value::Dict } primitive! { @@ -624,9 +623,15 @@ impl From for Value { } } +impl From for Value { + fn from(v: String) -> Self { + Self::Str(v.into()) + } +} + impl From<&str> for Value { fn from(v: &str) -> Self { - Self::Str(v.to_string()) + Self::Str(v.into()) } } diff --git a/src/exec/context.rs b/src/exec/context.rs index 0f6d47f59..4764a8086 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use super::{Exec, ExecWithMap, FontFamily, State}; use crate::diag::{Diag, DiagSet, Pass}; +use crate::eco::EcoString; use crate::eval::{ExprMap, TemplateValue}; use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size}; use crate::layout::{ @@ -77,7 +78,7 @@ impl ExecContext { /// Push text into the active paragraph. /// /// The text is split into lines at newlines. - pub fn push_text(&mut self, text: impl Into) { + pub fn push_text(&mut self, text: impl Into) { self.stack.par.push(self.make_text_node(text)); } @@ -143,7 +144,7 @@ impl ExecContext { Pass::new(self.tree, self.diags) } - fn make_text_node(&self, text: impl Into) -> ParChild { + fn make_text_node(&self, text: impl Into) -> ParChild { ParChild::Text( text.into(), self.state.aligns.cross, diff --git a/src/exec/mod.rs b/src/exec/mod.rs index fc829676f..2a145fbc9 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -6,6 +6,7 @@ mod state; pub use context::*; pub use state::*; +use std::fmt::Write; use std::rc::Rc; use crate::diag::Pass; @@ -13,6 +14,7 @@ use crate::eval::{ExprMap, TemplateFunc, TemplateNode, TemplateValue, Value}; use crate::geom::{Dir, Gen}; use crate::layout::{LayoutTree, StackChild, StackNode}; use crate::pretty::pretty; +use crate::eco::EcoString; use crate::syntax::*; /// Execute a template to produce a layout tree. @@ -102,18 +104,25 @@ impl ExecWithMap for HeadingNode { impl ExecWithMap for ListItem { fn exec_with_map(&self, ctx: &mut ExecContext, map: &ExprMap) { - exec_item(ctx, "•".to_string(), &self.body, map); + exec_item(ctx, '•'.into(), &self.body, map); } } impl ExecWithMap for EnumItem { fn exec_with_map(&self, ctx: &mut ExecContext, map: &ExprMap) { - let label = self.number.unwrap_or(1).to_string() + "."; + let mut label = EcoString::new(); + write!(&mut label, "{}", self.number.unwrap_or(1)).unwrap(); + label.push('.'); exec_item(ctx, label, &self.body, map); } } -fn exec_item(ctx: &mut ExecContext, label: String, body: &SyntaxTree, map: &ExprMap) { +fn exec_item( + ctx: &mut ExecContext, + label: EcoString, + body: &SyntaxTree, + map: &ExprMap, +) { let label = ctx.exec_stack(|ctx| ctx.push_text(label)); let body = ctx.exec_tree_stack(body, map); let stack = StackNode { diff --git a/src/layout/par.rs b/src/layout/par.rs index 5d21e05ea..bd7442016 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -6,6 +6,7 @@ use xi_unicode::LineBreakIterator; use super::*; use crate::exec::FontState; +use crate::eco::EcoString; use crate::util::{RangeExt, SliceExt}; type Range = std::ops::Range; @@ -29,7 +30,7 @@ pub enum ParChild { /// Spacing between other nodes. Spacing(Length), /// A run of text and how to align it in its line. - Text(String, Align, Rc), + Text(EcoString, Align, Rc), /// Any child node and how to align it in its line. Any(AnyNode, Align), } diff --git a/src/lib.rs b/src/lib.rs index f083163bc..000d61d3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ pub mod diag; pub mod eval; pub mod cache; pub mod color; +pub mod eco; pub mod exec; pub mod export; pub mod font; diff --git a/src/library/elements.rs b/src/library/elements.rs index c57504fb4..e669369a6 100644 --- a/src/library/elements.rs +++ b/src/library/elements.rs @@ -9,7 +9,7 @@ use crate::layout::{ /// `image`: An image. pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let path = args.expect::>(ctx, "path to image file"); + let path = args.expect::>(ctx, "path to image file"); let width = args.named(ctx, "width"); let height = args.named(ctx, "height"); diff --git a/src/library/layout.rs b/src/library/layout.rs index 4903077ba..28ee27e17 100644 --- a/src/library/layout.rs +++ b/src/library/layout.rs @@ -5,7 +5,7 @@ use crate::paper::{Paper, PaperClass}; /// `page`: Configure pages. pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let span = args.span; - let paper = args.eat::>(ctx).and_then(|name| { + let paper = args.eat::>(ctx).and_then(|name| { Paper::from_name(&name.v).or_else(|| { ctx.diag(error!(name.span, "invalid paper name")); None diff --git a/src/library/mod.rs b/src/library/mod.rs index abc9ff5fd..7c3e0a71c 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -17,6 +17,7 @@ use std::fmt::{self, Display, Formatter}; use std::rc::Rc; use crate::color::{Color, RgbaColor}; +use crate::eco::EcoString; use crate::eval::{EvalContext, FuncArgs, Scope, TemplateValue, Value}; use crate::exec::{Exec, FontFamily}; use crate::font::{FontStyle, FontWeight, VerticalFontMetric}; diff --git a/src/library/text.rs b/src/library/text.rs index e1fff9f30..a0ffc56c8 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -99,7 +99,7 @@ castable! { Value::Array(values) => Self(values .into_iter() .filter_map(|v| v.cast().ok()) - .map(|string: String| string.to_lowercase()) + .map(|string: EcoString| string.to_lowercase()) .collect() ), } @@ -185,7 +185,7 @@ pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { /// `lang`: Configure the language. pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let iso = args.eat::(ctx).map(|s| lang_dir(&s)); + let iso = args.eat::(ctx).map(|s| lang_dir(&s)); let dir = match args.named::>(ctx, "dir") { Some(dir) if dir.v.axis() == SpecAxis::Horizontal => Some(dir.v), Some(dir) => { diff --git a/src/library/utility.rs b/src/library/utility.rs index 272183aac..c1f20cc6d 100644 --- a/src/library/utility.rs +++ b/src/library/utility.rs @@ -39,7 +39,7 @@ pub fn len(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { /// `rgb`: Create an RGB(A) color. pub fn rgb(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { Value::Color(Color::Rgba( - if let Some(string) = args.eat::>(ctx) { + if let Some(string) = args.eat::>(ctx) { match RgbaColor::from_str(&string.v) { Ok(color) => color, Err(_) => { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index c3d532a74..4c2fd129f 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -15,6 +15,7 @@ pub use tokens::*; use std::rc::Rc; use crate::diag::Pass; +use crate::eco::EcoString; use crate::syntax::*; /// Parse a string of source code. @@ -151,10 +152,10 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option { } /// Handle a unicode escape sequence. -fn unicode_escape(p: &mut Parser, token: UnicodeEscapeToken) -> String { +fn unicode_escape(p: &mut Parser, token: UnicodeEscapeToken) -> EcoString { let span = p.peek_span(); let text = if let Some(c) = resolve::resolve_hex(token.sequence) { - c.to_string() + c.into() } else { // Print out the escape sequence verbatim if it is invalid. p.diag(error!(span, "invalid unicode escape sequence")); @@ -774,7 +775,7 @@ fn ident(p: &mut Parser) -> Option { if let Some(Token::Ident(string)) = p.peek() { Some(Ident { span: p.eat_span(), - string: string.to_string(), + string: string.into(), }) } else { p.expected("identifier"); diff --git a/src/parse/resolve.rs b/src/parse/resolve.rs index b3fdef4a0..c36762150 100644 --- a/src/parse/resolve.rs +++ b/src/parse/resolve.rs @@ -1,9 +1,10 @@ use super::{is_newline, Scanner}; +use crate::eco::EcoString; use crate::syntax::{Ident, RawNode, Span}; /// Resolve all escape sequences in a string. -pub fn resolve_string(string: &str) -> String { - let mut out = String::with_capacity(string.len()); +pub fn resolve_string(string: &str) -> EcoString { + let mut out = EcoString::with_capacity(string.len()); let mut s = Scanner::new(string); while let Some(c) = s.eat() { @@ -52,12 +53,12 @@ pub fn resolve_raw(span: Span, text: &str, backticks: usize) -> RawNode { let (tag, inner) = split_at_lang_tag(text); let (text, block) = trim_and_split_raw(inner); let lang = Ident::new(tag, span.start .. span.start + tag.len()); - RawNode { span, lang, text, block } + RawNode { span, lang, text: text.into(), block } } else { RawNode { span, lang: None, - text: split_lines(text).join("\n"), + text: split_lines(text).join("\n").into(), block: false, } } diff --git a/src/pretty.rs b/src/pretty.rs index 46b82eb7a..83c207b5e 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -617,7 +617,7 @@ mod tests { ($($k:ident: $v:expr),* $(,)?) => {{ #[allow(unused_mut)] let mut m = BTreeMap::new(); - $(m.insert(stringify!($k).to_string(), $v);)* + $(m.insert(stringify!($k).into(), $v);)* m }}; } diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index 368fe41f9..21df47c8d 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -28,7 +28,7 @@ pub enum Expr { /// A fraction unit literal: `1fr`. Fractional(Span, f64), /// A string literal: `"hello!"`. - Str(Span, String), + Str(Span, EcoString), /// An identifier: `left`. Ident(Ident), /// An array expression: `(1, "hi", 12cm)`. diff --git a/src/syntax/ident.rs b/src/syntax/ident.rs index a46ad2325..b1328bbe9 100644 --- a/src/syntax/ident.rs +++ b/src/syntax/ident.rs @@ -3,6 +3,7 @@ use std::ops::Deref; use unicode_xid::UnicodeXID; use super::Span; +use crate::eco::EcoString; /// An unicode identifier with a few extra permissible characters. /// @@ -16,7 +17,7 @@ pub struct Ident { /// The source code location. pub span: Span, /// The identifier string. - pub string: String, + pub string: EcoString, } impl Ident { @@ -26,7 +27,10 @@ impl Ident { span: impl Into, ) -> Option { if is_ident(string.as_ref()) { - Some(Self { span: span.into(), string: string.into() }) + Some(Self { + span: span.into(), + string: EcoString::from_str(string), + }) } else { None } @@ -34,13 +38,13 @@ impl Ident { /// Return a reference to the underlying string. pub fn as_str(&self) -> &str { - self.string.as_str() + self } } impl AsRef for Ident { fn as_ref(&self) -> &str { - self.as_str() + self } } @@ -48,7 +52,7 @@ impl Deref for Ident { type Target = str; fn deref(&self) -> &Self::Target { - self.as_str() + self.string.as_str() } } diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index 6673fda64..1de5c1dd9 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -13,6 +13,8 @@ pub use node::*; pub use span::*; pub use token::*; +use crate::eco::EcoString; + /// The abstract syntax tree. /// /// This type can represent a full parsed document. diff --git a/src/syntax/node.rs b/src/syntax/node.rs index 1fbdb3d8b..bb9ff0988 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -6,7 +6,7 @@ use super::*; #[derive(Debug, Clone, PartialEq)] pub enum Node { /// Plain text. - Text(String), + Text(EcoString), /// Whitespace containing less than two newlines. Space, /// A forced line break: `\`. @@ -38,7 +38,7 @@ pub struct RawNode { pub lang: Option, /// The raw text, determined as the raw string between the backticks trimmed /// according to the above rules. - pub text: String, + pub text: EcoString, /// Whether the element is block-level, that is, it has 3+ backticks /// and contains at least one newline. pub block: bool, diff --git a/tests/typ/code/ops.typ b/tests/typ/code/ops.typ index 58fd957fc..d8e873083 100644 --- a/tests/typ/code/ops.typ +++ b/tests/typ/code/ops.typ @@ -27,6 +27,7 @@ // Addition. #test(2 + 4, 6) #test("a" + "b", "ab") +#test(13 * "a" + "bbbbbb", "aaaaaaaaaaaaabbbbbb") #test((1, 2) + (3, 4), (1, 2, 3, 4)) #test((a: 1) + (b: 2, c: 3), (a: 1, b: 2, c: 3))