Repository: cobalt-org/liquid-rust
Pull request #371 (2019-12-18 02:53:28)

perf(value): Use new sstring for faster statics
BREAKING CHANGE: Internal string type has changed.

- [ ] Tests created for any new feature or regression tests for bugfixes.



Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 3f9788f..69ab8bb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
   "crates/derive",
   "crates/help-md",
   "crates/bin",
+  "crates/sstring",
 ]
 
 [package]
@@ -39,6 +40,7 @@ unicode-segmentation = "1.2"
 itertools = "0.8.0"
 url = "1.5"
 doc-comment = "0.3"
+sstring = { version = "0.1.0", path = "crates/sstring" }
 liquid-error = { version = "^0.19", path = "crates/error" }
 liquid-value = { version = "^0.19", path = "crates/value" }
 liquid-compiler = { version = "^0.19", path = "crates/compiler" }
diff --git a/crates/compiler/src/filter_chain.rs b/crates/compiler/src/filter_chain.rs
index 9c6f761..ea07462 100644
--- a/crates/compiler/src/filter_chain.rs
+++ b/crates/compiler/src/filter_chain.rs
@@ -57,7 +57,7 @@ impl fmt::Display for FilterChain {
 impl Renderable for FilterChain {
     fn render_to(&self, writer: &mut dyn Write, context: &mut Context) -> Result<()> {
         let entry = self.evaluate(context)?;
-        write!(writer, "{}", entry.to_str()).replace("Failed to render")?;
+        write!(writer, "{}", entry.render()).replace("Failed to render")?;
         Ok(())
     }
 }
diff --git a/crates/derive/src/filter_parameters.rs b/crates/derive/src/filter_parameters.rs
index ff8eaea..2649066 100644
--- a/crates/derive/src/filter_parameters.rs
+++ b/crates/derive/src/filter_parameters.rs
@@ -523,7 +523,7 @@ fn generate_evaluate_field(field: &FilterParameter) -> TokenStream {
             )?
         },
         FilterParameterType::Str => quote! {
-            .to_str()
+            .to_sstr()
         },
     };
 
@@ -671,7 +671,7 @@ fn generate_evaluated_struct(filter_parameters: &FilterParameters) -> TokenStrea
             FilterParameterType::Bool => quote! { bool },
             FilterParameterType::DateTime => quote! { ::liquid::value::DateTime },
             FilterParameterType::Date => quote! { ::liquid::value::Date },
-            FilterParameterType::Str => quote! { ::std::borrow::Cow<'a, str> },
+            FilterParameterType::Str => quote! { ::liquid::value::sstring::SStringCow<'a> },
         };
 
         if field.is_optional() {
diff --git a/crates/derive/src/lib.rs b/crates/derive/src/lib.rs
index 92bcfd9..e438394 100644
--- a/crates/derive/src/lib.rs
+++ b/crates/derive/src/lib.rs
@@ -64,7 +64,7 @@ use proc_macro::TokenStream;
 ///     - "date" -> only `Scalar(Date)` is accepted, `evaluate` will unwrap `Value`
 /// into `Date`.
 ///     - "str" -> only `Scalar(Str)` is accepted, `evaluate` will unwrap `Value`
-/// into `Cow<str>`.
+/// into `SStringCow`.
 ///
 /// # Examples
 ///
diff --git a/crates/error/Cargo.toml b/crates/error/Cargo.toml
index fde75aa..ed7aa57 100644
--- a/crates/error/Cargo.toml
+++ b/crates/error/Cargo.toml
@@ -15,3 +15,5 @@ edition = "2018"
 azure-devops = { project = "cobalt-org", pipeline = "liquid-rust" }
 
 [dependencies]
+# Exposed in API
+sstring = { version = "0.1.0", path = "../sstring" }
diff --git a/crates/error/src/error.rs b/crates/error/src/error.rs
index 014deba..f266111 100644
--- a/crates/error/src/error.rs
+++ b/crates/error/src/error.rs
@@ -1,4 +1,3 @@
-use std::borrow;
 use std::error;
 use std::fmt;
 use std::result;
@@ -23,18 +22,18 @@ pub struct Error {
 // shouldn't hurt.
 #[derive(Debug, Clone)]
 struct InnerError {
-    msg: borrow::Cow<'static, str>,
+    msg: sstring::SString,
     user_backtrace: Vec<Trace>,
     cause: Option<BoxedError>,
 }
 
 impl Error {
     /// Create a new compiler error with the given message
-    pub fn with_msg<S: Into<borrow::Cow<'static, str>>>(msg: S) -> Self {
+    pub fn with_msg<S: Into<sstring::SString>>(msg: S) -> Self {
         Self::with_msg_cow(msg.into())
     }
 
-    fn with_msg_cow(msg: borrow::Cow<'static, str>) -> Self {
+    fn with_msg_cow(msg: sstring::SString) -> Self {
         let error = InnerError {
             msg,
             user_backtrace: vec![Trace::empty()],
@@ -48,12 +47,12 @@ impl Error {
     /// Add a new call to the user-visible backtrace
     pub fn trace<T>(self, trace: T) -> Self
     where
-        T: Into<borrow::Cow<'static, str>>,
+        T: Into<sstring::SString>,
     {
         self.trace_trace(trace.into())
     }
 
-    fn trace_trace(mut self, trace: borrow::Cow<'static, str>) -> Self {
+    fn trace_trace(mut self, trace: sstring::SString) -> Self {
         let trace = Trace::new(trace);
         self.inner.user_backtrace.push(trace);
         self
@@ -64,17 +63,13 @@ impl Error {
     /// Example context: Value that parameters from the `trace` evaluate to.
     pub fn context<K, V>(self, key: K, value: V) -> Self
     where
-        K: Into<borrow::Cow<'static, str>>,
-        V: Into<borrow::Cow<'static, str>>,
+        K: Into<sstring::SString>,
+        V: Into<sstring::SString>,
     {
         self.context_cow_string(key.into(), value.into())
     }
 
-    fn context_cow_string(
-        mut self,
-        key: borrow::Cow<'static, str>,
-        value: borrow::Cow<'static, str>,
-    ) -> Self {
+    fn context_cow_string(mut self, key: sstring::SString, value: sstring::SString) -> Self {
         self.inner
             .user_backtrace
             .last_mut()
diff --git a/crates/error/src/result_ext.rs b/crates/error/src/result_ext.rs
index 13ebe4f..74cb9a3 100644
--- a/crates/error/src/result_ext.rs
+++ b/crates/error/src/result_ext.rs
@@ -1,4 +1,3 @@
-use std::borrow;
 use std::error;
 use std::result;
 
@@ -7,19 +6,17 @@ use super::Error;
 use super::ErrorClone;
 use super::Result;
 
-type CowStr = borrow::Cow<'static, str>;
-
 /// `Result` extension methods for adapting third party errors to `Error`.
 pub trait ResultLiquidChainExt<T> {
     /// Create an `Error` with `E` as the cause.
     #[must_use]
-    fn chain<S: Into<CowStr>>(self, msg: S) -> Result<T>;
+    fn chain<S: Into<sstring::SString>>(self, msg: S) -> Result<T>;
 
     /// Create an `Error` with `E` as the cause.
     #[must_use]
     fn chain_with<F>(self, msg: F) -> Result<T>
     where
-        F: FnOnce() -> CowStr;
+        F: FnOnce() -> sstring::SString;
 }
 
 /// `Result` extension methods for adapting third party errors to `Error`.
@@ -37,7 +34,7 @@ pub trait ResultLiquidReplaceExt<T> {
     /// let error: Result<i32> = error.lossy_chain("Missing liquid partial");
     /// ```
     #[must_use]
-    fn lossy_chain<S: Into<CowStr>>(self, msg: S) -> Result<T>;
+    fn lossy_chain<S: Into<sstring::SString>>(self, msg: S) -> Result<T>;
 
     /// Create an `Error` ignoring `E` as the cause.
     ///
@@ -56,7 +53,7 @@ pub trait ResultLiquidReplaceExt<T> {
     #[must_use]
     fn lossy_chain_with<F>(self, msg: F) -> Result<T>
     where
-        F: FnOnce() -> CowStr;
+        F: FnOnce() -> sstring::SString;
 
     /// Create an `Error` ignoring `E` as the cause.
     ///
@@ -71,7 +68,7 @@ pub trait ResultLiquidReplaceExt<T> {
     /// let error: Result<i32> = error.replace("Missing liquid partial");
     /// ```
     #[must_use]
-    fn replace<S: Into<CowStr>>(self, msg: S) -> Result<T>;
+    fn replace<S: Into<sstring::SString>>(self, msg: S) -> Result<T>;
 
     /// Create an `Error` ignoring `E` as the cause.
     ///
@@ -90,20 +87,20 @@ pub trait ResultLiquidReplaceExt<T> {
     #[must_use]
     fn replace_with<F>(self, msg: F) -> Result<T>
     where
-        F: FnOnce() -> CowStr;
+        F: FnOnce() -> sstring::SString;
 }
 
 impl<T, E> ResultLiquidChainExt<T> for result::Result<T, E>
 where
     E: ErrorClone,
 {
-    fn chain<S: Into<CowStr>>(self, msg: S) -> Result<T> {
+    fn chain<S: Into<sstring::SString>>(self, msg: S) -> Result<T> {
         self.map_err(|err| Error::with_msg(msg).cause(err))
     }
 
     fn chain_with<F>(self, msg: F) -> Result<T>
     where
-        F: FnOnce() -> CowStr,
+        F: FnOnce() -> sstring::SString,
     {
         self.map_err(|err| Error::with_msg(msg()).cause(err))
     }
@@ -113,24 +110,24 @@ impl<T, E> ResultLiquidReplaceExt<T> for result::Result<T, E>
 where
     E: error::Error + Send + Sync + 'static,
 {
-    fn lossy_chain<S: Into<CowStr>>(self, msg: S) -> Result<T> {
+    fn lossy_chain<S: Into<sstring::SString>>(self, msg: S) -> Result<T> {
         self.map_err(|err| Error::with_msg(msg).cause(CloneableError::new(err)))
     }
 
     fn lossy_chain_with<F>(self, msg: F) -> Result<T>
     where
-        F: FnOnce() -> CowStr,
+        F: FnOnce() -> sstring::SString,
     {
         self.map_err(|err| Error::with_msg(msg()).cause(CloneableError::new(err)))
     }
 
-    fn replace<S: Into<CowStr>>(self, msg: S) -> Result<T> {
+    fn replace<S: Into<sstring::SString>>(self, msg: S) -> Result<T> {
         self.map_err(|_| Error::with_msg(msg))
     }
 
     fn replace_with<F>(self, msg: F) -> Result<T>
     where
-        F: FnOnce() -> CowStr,
+        F: FnOnce() -> sstring::SString,
     {
         self.map_err(|_| Error::with_msg(msg()))
     }
@@ -156,7 +153,7 @@ where
     #[must_use]
     fn trace<S>(self, trace: S) -> Result<T>
     where
-        S: Into<CowStr>;
+        S: Into<sstring::SString>;
 
     /// Add a new stack frame to the `liquid_error::Error`.
     ///
@@ -174,7 +171,7 @@ where
     #[must_use]
     fn trace_with<F>(self, trace: F) -> Result<T>
     where
-        F: FnOnce() -> CowStr;
+        F: FnOnce() -> sstring::SString;
 
     /// Add state the current stack frame.
     ///
@@ -197,7 +194,7 @@ where
     #[must_use]
     fn context_key<S>(self, key: S) -> Key<T>
     where
-        S: Into<CowStr>;
+        S: Into<sstring::SString>;
 
     /// Add state the current stack frame.
     ///
@@ -220,34 +217,34 @@ where
     #[must_use]
     fn context_key_with<F>(self, key: F) -> FnKey<T, F>
     where
-        F: FnOnce() -> CowStr;
+        F: FnOnce() -> sstring::SString;
 }
 
 impl<T> ResultLiquidExt<T> for Result<T> {
     fn trace<S>(self, trace: S) -> Result<T>
     where
-        S: Into<CowStr>,
+        S: Into<sstring::SString>,
     {
         self.map_err(|err| err.trace(trace))
     }
 
     fn trace_with<F>(self, trace: F) -> Result<T>
     where
-        F: FnOnce() -> CowStr,
+        F: FnOnce() -> sstring::SString,
     {
         self.map_err(|err| err.trace(trace()))
     }
 
     fn context_key<S>(self, key: S) -> Key<T>
     where
-        S: Into<CowStr>,
+        S: Into<sstring::SString>,
     {
         Key::new(self, key)
     }
 
     fn context_key_with<F>(self, key: F) -> FnKey<T, F>
     where
-        F: FnOnce() -> CowStr,
+        F: FnOnce() -> sstring::SString,
     {
         FnKey::new(self, key)
     }
@@ -257,7 +254,7 @@ impl<T> ResultLiquidExt<T> for Result<T> {
 #[allow(missing_debug_implementations)]
 pub struct Key<T> {
     builder: Result<T>,
-    key: CowStr,
+    key: sstring::SString,
 }
 
 impl<T> Key<T> {
@@ -265,7 +262,7 @@ impl<T> Key<T> {
     #[must_use]
     pub fn new<S>(builder: Result<T>, key: S) -> Self
     where
-        S: Into<CowStr>,
+        S: Into<sstring::SString>,
     {
         Self {
             builder,
@@ -277,7 +274,7 @@ impl<T> Key<T> {
     #[must_use]
     pub fn value<S>(self, value: S) -> Result<T>
     where
-        S: Into<CowStr>,
+        S: Into<sstring::SString>,
     {
         let builder = self.builder;
         let key = self.key;
@@ -288,7 +285,7 @@ impl<T> Key<T> {
     #[must_use]
     pub fn value_with<F>(self, value: F) -> Result<T>
     where
-        F: FnOnce() -> CowStr,
+        F: FnOnce() -> sstring::SString,
     {
         let builder = self.builder;
         let key = self.key;
@@ -300,7 +297,7 @@ impl<T> Key<T> {
 #[allow(missing_debug_implementations)]
 pub struct FnKey<T, F>
 where
-    F: FnOnce() -> CowStr,
+    F: FnOnce() -> sstring::SString,
 {
     builder: Result<T>,
     key: F,
@@ -308,7 +305,7 @@ where
 
 impl<T, F> FnKey<T, F>
 where
-    F: FnOnce() -> CowStr,
+    F: FnOnce() -> sstring::SString,
 {
     /// Save off a key for a context that will be added to `builder`.
     #[must_use]
@@ -320,7 +317,7 @@ where
     #[must_use]
     pub fn value<S>(self, value: S) -> Result<T>
     where
-        S: Into<CowStr>,
+        S: Into<sstring::SString>,
     {
         let builder = self.builder;
         let key = self.key;
@@ -331,7 +328,7 @@ where
     #[must_use]
     pub fn value_with<V>(self, value: V) -> Result<T>
     where
-        V: FnOnce() -> CowStr,
+        V: FnOnce() -> sstring::SString,
     {
         let builder = self.builder;
         let key = self.key;
diff --git a/crates/error/src/trace.rs b/crates/error/src/trace.rs
index bc39f7d..714d842 100644
--- a/crates/error/src/trace.rs
+++ b/crates/error/src/trace.rs
@@ -1,14 +1,12 @@
-use std::borrow;
-
 /// User-visible call trace
 #[derive(Clone, PartialEq, Eq, Debug, Default)]
 pub(crate) struct Trace {
-    trace: Option<borrow::Cow<'static, str>>,
-    context: Vec<(borrow::Cow<'static, str>, borrow::Cow<'static, str>)>,
+    trace: Option<sstring::SString>,
+    context: Vec<(sstring::SString, sstring::SString)>,
 }
 
 impl Trace {
-    pub(crate) fn new(trace: borrow::Cow<'static, str>) -> Self {
+    pub(crate) fn new(trace: sstring::SString) -> Self {
         Self {
             trace: Some(trace),
             context: vec![],
@@ -22,19 +20,15 @@ impl Trace {
         }
     }
 
-    pub(crate) fn append_context(
-        &mut self,
-        key: borrow::Cow<'static, str>,
-        value: borrow::Cow<'static, str>,
-    ) {
+    pub(crate) fn append_context(&mut self, key: sstring::SString, value: sstring::SString) {
         self.context.push((key, value));
     }
 
     pub(crate) fn get_trace(&self) -> Option<&str> {
-        self.trace.as_ref().map(|s| s.as_ref())
+        self.trace.as_ref().map(|s| s.as_str())
     }
 
-    pub(crate) fn get_context(&self) -> &[(borrow::Cow<'static, str>, borrow::Cow<'static, str>)] {
+    pub(crate) fn get_context(&self) -> &[(sstring::SString, sstring::SString)] {
         self.context.as_ref()
     }
 }
diff --git a/crates/interpreter/Cargo.toml b/crates/interpreter/Cargo.toml
index ab7cb65..0bda5ef 100644
--- a/crates/interpreter/Cargo.toml
+++ b/crates/interpreter/Cargo.toml
@@ -18,6 +18,7 @@ azure-devops = { project = "cobalt-org", pipeline = "liquid-rust" }
 itertools = "0.8.0"
 anymap = "0.12"
 # Exposed in API
+sstring = { version = "0.1.0", path = "../sstring" }
 liquid-error = { version = "^0.19", path = "../error" }
 liquid-value = { version = "^0.19", path = "../value" }
 
diff --git a/crates/interpreter/src/context.rs b/crates/interpreter/src/context.rs
index fc9ff4a..b3bd239 100644
--- a/crates/interpreter/src/context.rs
+++ b/crates/interpreter/src/context.rs
@@ -188,7 +188,11 @@ impl<'g> Context<'g> {
     /// Sets up a new stack frame, executes the supplied function and then
     /// tears the stack frame down before returning the function's result
     /// to the caller.
-    pub fn run_in_named_scope<RvalT, S: Into<String>, FnT>(&mut self, name: S, f: FnT) -> RvalT
+    pub fn run_in_named_scope<RvalT, S: Into<sstring::SString>, FnT>(
+        &mut self,
+        name: S,
+        f: FnT,
+    ) -> RvalT
     where
         FnT: FnOnce(&mut Context<'_>) -> RvalT,
     {
diff --git a/crates/interpreter/src/stack.rs b/crates/interpreter/src/stack.rs
index d68c24d..5725f64 100644
--- a/crates/interpreter/src/stack.rs
+++ b/crates/interpreter/src/stack.rs
@@ -1,5 +1,3 @@
-use std::borrow;
-
 use itertools;
 use liquid_error::{Error, Result};
 use liquid_value::{Object, PathRef, Scalar, Value};
@@ -8,7 +6,7 @@ use super::ValueStore;
 
 #[derive(Clone, Default, Debug)]
 struct Frame {
-    name: Option<String>,
+    name: Option<sstring::SString>,
     data: Object,
 }
 
@@ -17,7 +15,7 @@ impl Frame {
         Default::default()
     }
 
-    fn with_name<S: Into<String>>(name: S) -> Self {
+    fn with_name<S: Into<sstring::SString>>(name: S) -> Self {
         Self {
             name: Some(name.into()),
             data: Object::new(),
@@ -58,7 +56,7 @@ impl<'g> Stack<'g> {
     }
 
     /// Creates a new variable scope chained to a parent scope.
-    pub(crate) fn push_named_frame<S: Into<String>>(&mut self, name: S) {
+    pub(crate) fn push_named_frame<S: Into<sstring::SString>>(&mut self, name: S) {
         self.stack.push(Frame::with_name(name));
     }
 
@@ -77,11 +75,11 @@ impl<'g> Stack<'g> {
     }
 
     /// The name of the currently active template.
-    pub fn frame_name(&self) -> Option<&str> {
+    pub fn frame_name(&self) -> Option<sstring::SStringRef<'_>> {
         self.stack
             .iter()
             .rev()
-            .find_map(|f| f.name.as_ref().map(|s| s.as_str()))
+            .find_map(|f| f.name.as_ref().map(|s| s.as_ref()))
     }
 
     /// Recursively index into the stack.
@@ -101,14 +99,14 @@ impl<'g> Stack<'g> {
                 .unwrap_or_else(|| Scalar::new("nil"));
             let globals = itertools::join(self.globals().iter(), ", ");
             Error::with_msg("Unknown variable")
-                .context("requested variable", key.to_str().into_owned())
+                .context("requested variable", key.to_sstr())
                 .context("available variables", globals)
         })?;
 
         frame.get_variable(path)
     }
 
-    fn globals(&self) -> Vec<&str> {
+    fn globals(&self) -> Vec<sstring::SStringRef<'_>> {
         let mut globals = self.globals.map(|g| g.roots()).unwrap_or_default();
         for frame in self.stack.iter() {
             globals.extend(frame.data.roots());
@@ -120,8 +118,8 @@ impl<'g> Stack<'g> {
 
     fn find_path_frame<'a>(&'a self, path: PathRef<'_, '_>) -> Option<&'a dyn ValueStore> {
         let key = path.iter().next()?;
-        let key = key.to_str();
-        self.find_frame(key.as_ref())
+        let key = key.to_sstr();
+        self.find_frame(key.as_str())
     }
 
     fn find_frame<'a>(&'a self, name: &str) -> Option<&'a dyn ValueStore> {
@@ -145,7 +143,7 @@ impl<'g> Stack<'g> {
     /// Used by increment and decrement tags
     pub fn set_index<S>(&mut self, name: S, val: Value) -> Option<Value>
     where
-        S: Into<borrow::Cow<'static, str>>,
+        S: Into<sstring::SString>,
     {
         self.indexes.insert(name.into(), val)
     }
@@ -158,9 +156,10 @@ impl<'g> Stack<'g> {
     /// Sets a value in the global context.
     pub fn set_global<S>(&mut self, name: S, val: Value) -> Option<Value>
     where
-        S: Into<borrow::Cow<'static, str>>,
+        S: Into<sstring::SString>,
     {
-        self.global_frame().insert(name.into(), val)
+        let name = name.into();
+        self.global_frame().insert(name, val)
     }
 
     /// Sets a value to the rendering context.
@@ -173,7 +172,7 @@ impl<'g> Stack<'g> {
     /// this should never happen in a well-formed program.
     pub fn set<S>(&mut self, name: S, val: Value) -> Option<Value>
     where
-        S: Into<borrow::Cow<'static, str>>,
+        S: Into<sstring::SString>,
     {
         self.current_frame().insert(name.into(), val)
     }
diff --git a/crates/interpreter/src/store.rs b/crates/interpreter/src/store.rs
index a8e4217..f74e7cf 100644
--- a/crates/interpreter/src/store.rs
+++ b/crates/interpreter/src/store.rs
@@ -13,7 +13,7 @@ pub trait ValueStore: fmt::Debug {
     fn contains_root(&self, name: &str) -> bool;
 
     /// Enumerate all root variables.
-    fn roots(&self) -> Vec<&str>;
+    fn roots(&self) -> Vec<sstring::SStringRef<'_>>;
 
     /// Check if variable exists.
     ///
@@ -42,7 +42,7 @@ impl ValueStore for Object {
         self.contains_key(name)
     }
 
-    fn roots(&self) -> Vec<&str> {
+    fn roots(&self) -> Vec<sstring::SStringRef<'_>> {
         self.keys().map(|s| s.as_ref()).collect()
     }
 
@@ -86,7 +86,7 @@ impl ValueStore for Object {
             let requested = path
                 .get(0)
                 .expect("`Path` guarantees at least one element")
-                .to_str()
+                .to_sstr()
                 .into_owned();
             let available = itertools::join(self.keys(), ", ");
             return Error::with_msg("Unknown variable")
@@ -100,8 +100,8 @@ impl ValueStore for Object {
 fn get_variable_option<'o>(obj: &'o Object, path: PathRef<'_, '_>) -> Option<&'o Value> {
     let mut indexes = path.iter();
     let key = indexes.next()?;
-    let key = key.to_str();
-    let value = obj.get(key.as_ref())?;
+    let key = key.to_sstr();
+    let value = obj.get(key.as_str())?;
 
     indexes.fold(Some(value), |value, index| {
         let value = value?;
diff --git a/crates/interpreter/src/variable.rs b/crates/interpreter/src/variable.rs
index 205d7f7..a8b8784 100644
--- a/crates/interpreter/src/variable.rs
+++ b/crates/interpreter/src/variable.rs
@@ -105,7 +105,7 @@ test_a: ["test"]
         let context = ContextBuilder::new().set_globals(&globals).build();
         let actual = var.evaluate(&context).unwrap();
         let actual = context.stack().get(&actual).unwrap();
-        assert_eq!(actual.to_str(), "test");
+        assert_eq!(actual, "test");
     }
 
     #[test]
@@ -123,7 +123,7 @@ test_a: ["test1", "test2"]
         let context = ContextBuilder::new().set_globals(&globals).build();
         let actual = var.evaluate(&context).unwrap();
         let actual = context.stack().get(&actual).unwrap();
-        assert_eq!(actual.to_str(), "test2");
+        assert_eq!(actual, "test2");
     }
 
     #[test]
@@ -142,6 +142,6 @@ test_a:
         let context = ContextBuilder::new().set_globals(&globals).build();
         let actual = var.evaluate(&context).unwrap();
         let actual = context.stack().get(&actual).unwrap();
-        assert_eq!(actual.to_str(), "5");
+        assert_eq!(actual, &5);
     }
 }
diff --git a/crates/sstring/Cargo.toml b/crates/sstring/Cargo.toml
new file mode 100644
index 0000000..246c032
--- /dev/null
+++ b/crates/sstring/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "sstring"
+version = "0.1.0"
+authors = ["Ed Page <eopage@gmail.com>"]
+description = "String library optimized for static declarations"
+repository = "https://github.com/cobalt-org/liquid-rust"
+documentation = "https://docs.rs/sstring"
+readme = "README.md"
+license = "MIT OR Apache-2.0"
+edition = "2018"
+
+[badges]
+azure-devops = { project = "cobalt-org", pipeline = "liquid-rust" }
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
diff --git a/crates/sstring/README.md b/crates/sstring/README.md
new file mode 100644
index 0000000..9888765
--- /dev/null
+++ b/crates/sstring/README.md
@@ -0,0 +1,23 @@
+SString
+===========
+
+> Immutable string type optimized for static data.
+
+[![Build Status](https://dev.azure.com/cobalt-org/cobalt-org/_apis/build/status/liquid-rust?branchName=master)](https://dev.azure.com/cobalt-org/cobalt-org/_build/latest?definitionId=1&branchName=master)
+[![Crates Status](https://img.shields.io/crates/v/sstring.svg)](https://crates.io/crates/sstring)
+
+## License
+
+Licensed under either of
+
+ * Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0)
+ * MIT license (http://opensource.org/licenses/MIT)
+
+at your option.
+
+### Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally
+submitted for inclusion in the work by you, as defined in the Apache-2.0
+license, shall be dual licensed as above, without any additional terms or
+conditions.
diff --git a/crates/sstring/src/cow.rs b/crates/sstring/src/cow.rs
new file mode 100644
index 0000000..f77599d
--- /dev/null
+++ b/crates/sstring/src/cow.rs
@@ -0,0 +1,265 @@
+use std::fmt;
+
+use crate::SString;
+use crate::SStringInner;
+use crate::SStringRef;
+use crate::SStringRefInner;
+
+type StdString = std::string::String;
+
+/// A reference to a UTF-8 encoded, immutable string.
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
+#[serde(transparent)]
+#[repr(transparent)]
+pub struct SStringCow<'s> {
+    #[serde(with = "serde_string_cow")]
+    pub(crate) inner: SStringCowInner<'s>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub(crate) enum SStringCowInner<'s> {
+    Owned(StdString),
+    Borrowed(&'s str),
+    Singleton(&'static str),
+}
+
+impl<'s> SStringCow<'s> {
+    /// Create a new empty `SString`.
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    /// Create an owned `SString`.
+    pub fn owned(other: impl Into<SString>) -> Self {
+        let other = other.into();
+        other.into()
+    }
+
+    /// Create a reference to a borrowed data.
+    pub fn borrow(other: impl Into<SStringRef<'s>>) -> Self {
+        let other = other.into();
+        other.into()
+    }
+
+    /// Create a reference to a `'static` data.
+    pub fn singleton(other: &'static str) -> Self {
+        Self {
+            inner: SStringCowInner::Singleton(other),
+        }
+    }
+
+    /// Get a reference to the `SString`.
+    pub fn as_ref(&self) -> SStringRef<'_> {
+        match self.inner {
+            SStringCowInner::Owned(ref s) => SStringRef::borrow(s),
+            SStringCowInner::Borrowed(ref s) => SStringRef::borrow(*s),
+            SStringCowInner::Singleton(ref s) => SStringRef::singleton(s),
+        }
+    }
+
+    /// Clone the data into an owned-type.
+    pub fn into_owned(self) -> SString {
+        match self.inner {
+            SStringCowInner::Owned(s) => s.into(),
+            SStringCowInner::Borrowed(s) => s.to_owned().into(),
+            SStringCowInner::Singleton(s) => s.into(),
+        }
+    }
+
+    /// Extracts a string slice containing the entire `SStringCow`.
+    pub fn as_str(&self) -> &str {
+        match self.inner {
+            SStringCowInner::Owned(ref s) => s.as_str(),
+            SStringCowInner::Borrowed(ref s) => s,
+            SStringCowInner::Singleton(ref s) => s,
+        }
+    }
+
+    /// Convert to a mutable string type, cloning the data if necessary.
+    pub fn into_mut(self) -> StdString {
+        match self.inner {
+            SStringCowInner::Owned(s) => s,
+            SStringCowInner::Borrowed(s) => s.to_owned(),
+            SStringCowInner::Singleton(s) => s.to_owned(),
+        }
+    }
+}
+
+impl<'s> std::ops::Deref for SStringCow<'s> {
+    type Target = str;
+
+    #[inline]
+    fn deref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl<'s> Eq for SStringCow<'s> {}
+
+impl<'s> PartialEq<SStringCow<'s>> for SStringCow<'s> {
+    #[inline]
+    fn eq(&self, other: &SStringCow<'s>) -> bool {
+        PartialEq::eq(self.as_str(), other.as_str())
+    }
+}
+
+impl<'s> PartialEq<str> for SStringCow<'s> {
+    #[inline]
+    fn eq(&self, other: &str) -> bool {
+        PartialEq::eq(self.as_str(), other)
+    }
+}
+
+impl<'s> PartialEq<&'s str> for SStringCow<'s> {
+    #[inline]
+    fn eq(&self, other: &&str) -> bool {
+        PartialEq::eq(self.as_str(), *other)
+    }
+}
+
+impl<'s> PartialEq<String> for SStringCow<'s> {
+    #[inline]
+    fn eq(&self, other: &StdString) -> bool {
+        PartialEq::eq(self.as_str(), other.as_str())
+    }
+}
+
+impl<'s> Ord for SStringCow<'s> {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.as_str().cmp(other.as_str())
+    }
+}
+
+impl<'s> PartialOrd for SStringCow<'s> {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        self.as_str().partial_cmp(other.as_str())
+    }
+}
+
+impl<'s> std::hash::Hash for SStringCow<'s> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.as_str().hash(state);
+    }
+}
+
+impl<'s> fmt::Display for SStringCow<'s> {
+    #[inline]
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(self.as_str(), f)
+    }
+}
+
+impl<'s> AsRef<str> for SStringCow<'s> {
+    #[inline]
+    fn as_ref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl<'s> AsRef<[u8]> for SStringCow<'s> {
+    #[inline]
+    fn as_ref(&self) -> &[u8] {
+        self.as_bytes()
+    }
+}
+
+impl<'s> AsRef<std::ffi::OsStr> for SStringCow<'s> {
+    fn as_ref(&self) -> &std::ffi::OsStr {
+        (&**self).as_ref()
+    }
+}
+
+impl<'s> AsRef<std::path::Path> for SStringCow<'s> {
+    fn as_ref(&self) -> &std::path::Path {
+        std::path::Path::new(self)
+    }
+}
+
+impl<'s> std::borrow::Borrow<str> for SStringCow<'s> {
+    #[inline]
+    fn borrow(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl<'s> Default for SStringCow<'s> {
+    fn default() -> Self {
+        Self::singleton("")
+    }
+}
+
+impl<'s> From<SString> for SStringCow<'s> {
+    fn from(other: SString) -> Self {
+        match other.inner {
+            SStringInner::Owned(s) => s.into(),
+            SStringInner::Singleton(s) => SStringCow::singleton(s),
+        }
+    }
+}
+
+impl<'s> From<SStringRef<'s>> for SStringCow<'s> {
+    fn from(other: SStringRef<'s>) -> Self {
+        match other.inner {
+            SStringRefInner::Borrowed(s) => s.into(),
+            SStringRefInner::Singleton(s) => SStringCow::singleton(s),
+        }
+    }
+}
+
+impl<'s> From<&'s SStringRef<'s>> for SStringCow<'s> {
+    fn from(other: &'s SStringRef<'s>) -> Self {
+        match other.inner {
+            SStringRefInner::Borrowed(s) => s.into(),
+            SStringRefInner::Singleton(s) => SStringCow::singleton(s),
+        }
+    }
+}
+
+impl<'s> From<StdString> for SStringCow<'s> {
+    fn from(other: StdString) -> Self {
+        SStringCow {
+            inner: SStringCowInner::Owned(other),
+        }
+    }
+}
+
+impl<'s> From<&'s StdString> for SStringCow<'s> {
+    fn from(other: &'s StdString) -> Self {
+        SStringCow {
+            inner: SStringCowInner::Borrowed(other.as_str()),
+        }
+    }
+}
+
+impl<'s> From<&'s str> for SStringCow<'s> {
+    fn from(other: &'s str) -> Self {
+        SStringCow {
+            inner: SStringCowInner::Borrowed(other),
+        }
+    }
+}
+
+mod serde_string_cow {
+    use super::*;
+    use serde::{self, Deserialize, Deserializer, Serializer};
+
+    pub(crate) fn serialize<S>(data: &SStringCowInner, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let s = match data {
+            SStringCowInner::Owned(ref s) => s.as_str(),
+            SStringCowInner::Borrowed(ref s) => s,
+            SStringCowInner::Singleton(ref s) => s,
+        };
+        serializer.serialize_str(&s)
+    }
+
+    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<SStringCowInner<'static>, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = StdString::deserialize(deserializer)?;
+        Ok(SStringCowInner::Owned(s))
+    }
+}
diff --git a/crates/sstring/src/lib.rs b/crates/sstring/src/lib.rs
new file mode 100644
index 0000000..0abdbf4
--- /dev/null
+++ b/crates/sstring/src/lib.rs
@@ -0,0 +1,7 @@
+mod cow;
+mod r#ref;
+mod string;
+
+pub use cow::*;
+pub use r#ref::*;
+pub use string::*;
diff --git a/crates/sstring/src/ref.rs b/crates/sstring/src/ref.rs
new file mode 100644
index 0000000..3f939fe
--- /dev/null
+++ b/crates/sstring/src/ref.rs
@@ -0,0 +1,193 @@
+use std::fmt;
+
+use crate::SString;
+use crate::SStringCow;
+
+type StdString = std::string::String;
+
+/// A reference to a UTF-8 encoded, immutable string.
+#[derive(Copy, Clone, Debug)]
+#[repr(transparent)]
+pub struct SStringRef<'s> {
+    pub(crate) inner: SStringRefInner<'s>,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub(crate) enum SStringRefInner<'s> {
+    Borrowed(&'s str),
+    Singleton(&'static str),
+}
+
+impl<'s> SStringRef<'s> {
+    /// Create a new empty `SString`.
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    /// Create a reference to a borrowed data.
+    pub fn borrow(other: impl Into<SStringRef<'s>>) -> Self {
+        other.into()
+    }
+
+    /// Create a reference to a `'static` data.
+    pub fn singleton(other: &'static str) -> Self {
+        Self {
+            inner: SStringRefInner::Singleton(other),
+        }
+    }
+
+    /// Clone the data into an owned-type.
+    pub fn to_owned(&self) -> SString {
+        match self.inner {
+            SStringRefInner::Borrowed(s) => s.to_owned().into(),
+            SStringRefInner::Singleton(s) => s.into(),
+        }
+    }
+
+    /// Extracts a string slice containing the entire `SStringRef`.
+    pub fn as_str(&self) -> &str {
+        match self.inner {
+            SStringRefInner::Borrowed(ref s) => s,
+            SStringRefInner::Singleton(ref s) => s,
+        }
+    }
+
+    /// Convert to a mutable string type, cloning the data if necessary.
+    pub fn into_mut(self) -> StdString {
+        match self.inner {
+            SStringRefInner::Borrowed(s) => s.to_owned(),
+            SStringRefInner::Singleton(s) => s.to_owned(),
+        }
+    }
+}
+
+impl<'s> std::ops::Deref for SStringRef<'s> {
+    type Target = str;
+
+    #[inline]
+    fn deref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl<'s> Eq for SStringRef<'s> {}
+
+impl<'s> PartialEq<SStringRef<'s>> for SStringRef<'s> {
+    #[inline]
+    fn eq(&self, other: &SStringRef<'s>) -> bool {
+        PartialEq::eq(self.as_str(), other.as_str())
+    }
+}
+
+impl<'s> PartialEq<str> for SStringRef<'s> {
+    #[inline]
+    fn eq(&self, other: &str) -> bool {
+        PartialEq::eq(self.as_str(), other)
+    }
+}
+
+impl<'s> PartialEq<&'s str> for SStringRef<'s> {
+    #[inline]
+    fn eq(&self, other: &&str) -> bool {
+        PartialEq::eq(self.as_str(), *other)
+    }
+}
+
+impl<'s> PartialEq<String> for SStringRef<'s> {
+    #[inline]
+    fn eq(&self, other: &StdString) -> bool {
+        PartialEq::eq(self.as_str(), other.as_str())
+    }
+}
+
+impl<'s> Ord for SStringRef<'s> {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.as_str().cmp(other.as_str())
+    }
+}
+
+impl<'s> PartialOrd for SStringRef<'s> {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        self.as_str().partial_cmp(other.as_str())
+    }
+}
+
+impl<'s> std::hash::Hash for SStringRef<'s> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.as_str().hash(state);
+    }
+}
+
+impl<'s> fmt::Display for SStringRef<'s> {
+    #[inline]
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(self.as_str(), f)
+    }
+}
+
+impl<'s> AsRef<str> for SStringRef<'s> {
+    #[inline]
+    fn as_ref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl<'s> AsRef<[u8]> for SStringRef<'s> {
+    #[inline]
+    fn as_ref(&self) -> &[u8] {
+        self.as_bytes()
+    }
+}
+
+impl<'s> AsRef<std::ffi::OsStr> for SStringRef<'s> {
+    fn as_ref(&self) -> &std::ffi::OsStr {
+        (&**self).as_ref()
+    }
+}
+
+impl<'s> AsRef<std::path::Path> for SStringRef<'s> {
+    fn as_ref(&self) -> &std::path::Path {
+        std::path::Path::new(self)
+    }
+}
+
+impl<'s> std::borrow::Borrow<str> for SStringRef<'s> {
+    #[inline]
+    fn borrow(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl<'s> Default for SStringRef<'s> {
+    fn default() -> Self {
+        Self::singleton("")
+    }
+}
+
+impl<'s> From<&'s SString> for SStringRef<'s> {
+    fn from(other: &'s SString) -> Self {
+        other.as_ref()
+    }
+}
+
+impl<'s> From<&'s SStringCow<'s>> for SStringRef<'s> {
+    fn from(other: &'s SStringCow<'s>) -> Self {
+        other.as_ref()
+    }
+}
+
+impl<'s> From<&'s StdString> for SStringRef<'s> {
+    fn from(other: &'s StdString) -> Self {
+        SStringRef {
+            inner: SStringRefInner::Borrowed(other.as_str()),
+        }
+    }
+}
+
+impl<'s> From<&'s str> for SStringRef<'s> {
+    fn from(other: &'s str) -> Self {
+        SStringRef {
+            inner: SStringRefInner::Borrowed(other),
+        }
+    }
+}
diff --git a/crates/sstring/src/string.rs b/crates/sstring/src/string.rs
new file mode 100644
index 0000000..71616fd
--- /dev/null
+++ b/crates/sstring/src/string.rs
@@ -0,0 +1,233 @@
+use std::fmt;
+
+use crate::SStringCow;
+use crate::SStringRef;
+
+type StdString = std::string::String;
+
+/// A UTF-8 encoded, immutable string.
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
+#[serde(transparent)]
+#[repr(transparent)]
+pub struct SString {
+    #[serde(with = "serde_string")]
+    pub(crate) inner: SStringInner,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub(crate) enum SStringInner {
+    Owned(StdString),
+    Singleton(&'static str),
+}
+
+impl SString {
+    /// Create a new empty `SString`.
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    /// Create an owned `SString`.
+    pub fn owned(other: impl Into<StdString>) -> Self {
+        Self {
+            inner: SStringInner::Owned(other.into()),
+        }
+    }
+
+    /// Create a reference to a `'static` data.
+    pub fn singleton(other: &'static str) -> Self {
+        Self {
+            inner: SStringInner::Singleton(other),
+        }
+    }
+
+    /// Get a reference to the `SString`.
+    pub fn as_ref(&self) -> SStringRef<'_> {
+        match self.inner {
+            SStringInner::Owned(ref s) => SStringRef::borrow(s),
+            SStringInner::Singleton(ref s) => SStringRef::singleton(s),
+        }
+    }
+
+    /// Extracts a string slice containing the entire `SString`.
+    pub fn as_str(&self) -> &str {
+        match self.inner {
+            SStringInner::Owned(ref s) => s.as_str(),
+            SStringInner::Singleton(ref s) => s,
+        }
+    }
+
+    /// Convert to a mutable string type, cloning the data if necessary.
+    pub fn into_mut(self) -> StdString {
+        match self.inner {
+            SStringInner::Owned(s) => s,
+            SStringInner::Singleton(s) => s.to_owned(),
+        }
+    }
+}
+
+impl std::ops::Deref for SString {
+    type Target = str;
+
+    #[inline]
+    fn deref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl Eq for SString {}
+
+impl<'s> PartialEq<SString> for SString {
+    #[inline]
+    fn eq(&self, other: &SString) -> bool {
+        PartialEq::eq(self.as_str(), other.as_str())
+    }
+}
+
+impl<'s> PartialEq<str> for SString {
+    #[inline]
+    fn eq(&self, other: &str) -> bool {
+        PartialEq::eq(self.as_str(), other)
+    }
+}
+
+impl<'s> PartialEq<&'s str> for SString {
+    #[inline]
+    fn eq(&self, other: &&str) -> bool {
+        PartialEq::eq(self.as_str(), *other)
+    }
+}
+
+impl<'s> PartialEq<String> for SString {
+    #[inline]
+    fn eq(&self, other: &StdString) -> bool {
+        PartialEq::eq(self.as_str(), other.as_str())
+    }
+}
+
+impl Ord for SString {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.as_str().cmp(other.as_str())
+    }
+}
+
+impl PartialOrd for SString {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        self.as_str().partial_cmp(other.as_str())
+    }
+}
+
+impl std::hash::Hash for SString {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.as_str().hash(state);
+    }
+}
+
+impl fmt::Display for SString {
+    #[inline]
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(self.as_str(), f)
+    }
+}
+
+impl AsRef<str> for SString {
+    #[inline]
+    fn as_ref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl AsRef<[u8]> for SString {
+    #[inline]
+    fn as_ref(&self) -> &[u8] {
+        self.as_bytes()
+    }
+}
+
+impl AsRef<std::ffi::OsStr> for SString {
+    fn as_ref(&self) -> &std::ffi::OsStr {
+        (&**self).as_ref()
+    }
+}
+
+impl AsRef<std::path::Path> for SString {
+    fn as_ref(&self) -> &std::path::Path {
+        std::path::Path::new(self)
+    }
+}
+
+impl std::borrow::Borrow<str> for SString {
+    #[inline]
+    fn borrow(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl Default for SString {
+    fn default() -> Self {
+        "".into()
+    }
+}
+
+impl<'s> From<SStringRef<'s>> for SString {
+    fn from(other: SStringRef<'s>) -> Self {
+        other.to_owned()
+    }
+}
+
+impl<'s> From<&'s SStringRef<'s>> for SString {
+    fn from(other: &'s SStringRef<'s>) -> Self {
+        other.to_owned()
+    }
+}
+
+impl<'s> From<SStringCow<'s>> for SString {
+    fn from(other: SStringCow<'s>) -> Self {
+        other.into_owned()
+    }
+}
+
+impl<'s> From<&'s SStringCow<'s>> for SString {
+    fn from(other: &'s SStringCow<'s>) -> Self {
+        other.clone().into_owned()
+    }
+}
+
+impl From<StdString> for SString {
+    fn from(other: StdString) -> Self {
+        SString {
+            inner: SStringInner::Owned(other),
+        }
+    }
+}
+
+impl From<&'static str> for SString {
+    fn from(other: &'static str) -> Self {
+        SString {
+            inner: SStringInner::Singleton(other),
+        }
+    }
+}
+
+mod serde_string {
+    use super::*;
+    use serde::{self, Deserialize, Deserializer, Serializer};
+
+    pub(crate) fn serialize<S>(data: &SStringInner, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let s = match data {
+            SStringInner::Owned(ref s) => s.as_str(),
+            SStringInner::Singleton(ref s) => s,
+        };
+        serializer.serialize_str(&s)
+    }
+
+    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<SStringInner, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = StdString::deserialize(deserializer)?;
+        Ok(SStringInner::Owned(s))
+    }
+}
diff --git a/crates/value/Cargo.toml b/crates/value/Cargo.toml
index ebd3cf2..60940ec 100644
--- a/crates/value/Cargo.toml
+++ b/crates/value/Cargo.toml
@@ -20,6 +20,7 @@ num-traits = "0.2"
 # Exposed in API
 chrono = "0.4"
 serde = { version = "1.0", features = ["derive"] }
+sstring = { version = "0.1.0", path = "../sstring" }
 liquid-error = { version = "^0.19", path = "../error" }
 
 [dev-dependencies]
diff --git a/crates/value/src/date.rs b/crates/value/src/date.rs
index 4e7a89f..4a13dd6 100644
--- a/crates/value/src/date.rs
+++ b/crates/value/src/date.rs
@@ -10,6 +10,7 @@ pub type FixedOffset = chrono::FixedOffset;
     Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
 )]
 #[serde(transparent)]
+#[repr(transparent)]
 pub struct DateTime {
     #[serde(with = "friendly_date_time")]
     inner: DateTimeImpl,
diff --git a/crates/value/src/lib.rs b/crates/value/src/lib.rs
index b207a9b..3bf231d 100644
--- a/crates/value/src/lib.rs
+++ b/crates/value/src/lib.rs
@@ -23,6 +23,11 @@ pub mod error {
     pub use liquid_error::*;
 }
 
+/// String-type optimized for `Value`
+pub mod sstring {
+    pub use sstring::*;
+}
+
 pub use crate::date::*;
 pub use crate::path::*;
 pub use crate::scalar::*;
diff --git a/crates/value/src/macros.rs b/crates/value/src/macros.rs
index 5855b03..a198b76 100644
--- a/crates/value/src/macros.rs
+++ b/crates/value/src/macros.rs
@@ -12,7 +12,7 @@
 ///     .to_integer().unwrap();
 /// liquid_value!("foo")
 ///     .as_scalar().unwrap()
-///     .to_str();
+///     .to_sstr();
 /// liquid_value!([1, 2, 3])
 ///     .as_array().unwrap();
 /// liquid_value!({"foo": 5})
diff --git a/crates/value/src/map.rs b/crates/value/src/map.rs
index e64984a..66ba898 100644
--- a/crates/value/src/map.rs
+++ b/crates/value/src/map.rs
@@ -3,7 +3,7 @@
 //! This is to abstract the choice of map from the user so it can be changed without breaking
 //! compatibility.
 
-use std::borrow::{Borrow, Cow};
+use std::borrow::Borrow;
 use std::collections::hash_map;
 use std::fmt::{self, Debug};
 use std::hash::Hash;
@@ -19,7 +19,7 @@ pub struct Map {
     map: MapImpl<Key, Value>,
 }
 
-type Key = Cow<'static, str>;
+type Key = sstring::SString;
 
 type MapImpl<K, V> = hash_map::HashMap<K, V>;
 type VacantEntryImpl<'a> = hash_map::VacantEntry<'a, Key, Value>;
@@ -212,9 +212,9 @@ impl Eq for Map {}
 /// # let val = &Value::scalar("");
 /// # let _ =
 /// match *val {
-///     Value::Scalar(ref s) => Some(s.to_str()),
-///     Value::Array(ref arr) => Some(arr[0].to_str()),
-///     Value::Object(ref map) => Some(map["type"].to_str()),
+///     Value::Scalar(ref s) => Some(s.to_sstr()),
+///     Value::Array(ref arr) => Some(arr[0].to_sstr()),
+///     Value::Object(ref map) => Some(map["type"].to_sstr()),
 ///     _ => None,
 /// }
 /// # ;
diff --git a/crates/value/src/scalar.rs b/crates/value/src/scalar.rs
index e65b414..9e12a76 100644
--- a/crates/value/src/scalar.rs
+++ b/crates/value/src/scalar.rs
@@ -1,11 +1,16 @@
-use std::borrow;
 use std::cmp::Ordering;
 use std::fmt;
 
+use sstring::SString;
+use sstring::SStringCow;
+use sstring::SStringRef;
+
 use crate::{Date, DateTime};
 
 /// A Liquid scalar value
 #[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(transparent)]
+#[repr(transparent)]
 pub struct ScalarCow<'s>(ScalarCowEnum<'s>);
 
 /// A Liquid scalar value
@@ -20,7 +25,7 @@ enum ScalarCowEnum<'s> {
     Bool(bool),
     DateTime(DateTime),
     Date(Date),
-    Str(borrow::Cow<'s, str>),
+    Str(SStringCow<'s>),
 }
 
 impl<'s> ScalarCow<'s> {
@@ -60,25 +65,25 @@ impl<'s> ScalarCow<'s> {
     }
 
     /// Interpret as a string.
-    pub fn to_str(&self) -> borrow::Cow<'_, str> {
+    pub fn to_sstr(&self) -> SStringCow<'_> {
         match self.0 {
             ScalarCowEnum::Integer(_)
             | ScalarCowEnum::Float(_)
             | ScalarCowEnum::Bool(_)
             | ScalarCowEnum::DateTime(_)
-            | ScalarCowEnum::Date(_) => borrow::Cow::Owned(self.render().to_string()),
-            ScalarCowEnum::Str(ref x) => borrow::Cow::Borrowed(x.as_ref()),
+            | ScalarCowEnum::Date(_) => self.render().to_string().into(),
+            ScalarCowEnum::Str(ref x) => x.as_ref().into(),
         }
     }
 
     /// Convert to a string.
-    pub fn into_string(self) -> String {
+    pub fn into_string(self) -> SString {
         match self.0 {
-            ScalarCowEnum::Integer(x) => x.to_string(),
-            ScalarCowEnum::Float(x) => x.to_string(),
-            ScalarCowEnum::Bool(x) => x.to_string(),
-            ScalarCowEnum::DateTime(x) => x.to_string(),
-            ScalarCowEnum::Date(x) => x.to_string(),
+            ScalarCowEnum::Integer(x) => x.to_string().into(),
+            ScalarCowEnum::Float(x) => x.to_string().into(),
+            ScalarCowEnum::Bool(x) => x.to_string().into(),
+            ScalarCowEnum::DateTime(x) => x.to_string().into(),
+            ScalarCowEnum::Date(x) => x.to_string().into(),
             ScalarCowEnum::Str(x) => x.into_owned(),
         }
     }
@@ -114,7 +119,7 @@ impl<'s> ScalarCow<'s> {
     pub fn to_date_time(&self) -> Option<DateTime> {
         match self.0 {
             ScalarCowEnum::DateTime(ref x) => Some(*x),
-            ScalarCowEnum::Str(ref x) => DateTime::from_str(x.as_ref()),
+            ScalarCowEnum::Str(ref x) => DateTime::from_str(x.as_str()),
             _ => None,
         }
     }
@@ -124,7 +129,7 @@ impl<'s> ScalarCow<'s> {
         match self.0 {
             ScalarCowEnum::DateTime(ref x) => Some(x.date()),
             ScalarCowEnum::Date(ref x) => Some(*x),
-            ScalarCowEnum::Str(ref x) => Date::from_str(x.as_ref()),
+            ScalarCowEnum::Str(ref x) => Date::from_str(x.as_str()),
             _ => None,
         }
     }
@@ -201,6 +206,54 @@ impl<'s> From<Date> for ScalarCow<'s> {
     }
 }
 
+impl<'s> From<SString> for ScalarCow<'s> {
+    fn from(s: SString) -> Self {
+        ScalarCow {
+            0: ScalarCowEnum::Str(s.into()),
+        }
+    }
+}
+
+impl<'s> From<&'s SString> for ScalarCow<'s> {
+    fn from(s: &'s SString) -> Self {
+        ScalarCow {
+            0: ScalarCowEnum::Str(s.as_ref().into()),
+        }
+    }
+}
+
+impl<'s> From<SStringRef<'s>> for ScalarCow<'s> {
+    fn from(s: SStringRef<'s>) -> Self {
+        ScalarCow {
+            0: ScalarCowEnum::Str(s.into()),
+        }
+    }
+}
+
+impl<'s> From<&'s SStringRef<'s>> for ScalarCow<'s> {
+    fn from(s: &'s SStringRef<'s>) -> Self {
+        ScalarCow {
+            0: ScalarCowEnum::Str(s.into()),
+        }
+    }
+}
+
+impl<'s> From<SStringCow<'s>> for ScalarCow<'s> {
+    fn from(s: SStringCow<'s>) -> Self {
+        ScalarCow {
+            0: ScalarCowEnum::Str(s),
+        }
+    }
+}
+
+impl<'s> From<&'s SStringCow<'s>> for ScalarCow<'s> {
+    fn from(s: &'s SStringCow<'s>) -> Self {
+        ScalarCow {
+            0: ScalarCowEnum::Str(s.clone()),
+        }
+    }
+}
+
 impl<'s> From<String> for ScalarCow<'s> {
     fn from(s: String) -> Self {
         ScalarCow {
@@ -225,14 +278,6 @@ impl<'s> From<&'s str> for ScalarCow<'s> {
     }
 }
 
-impl<'s> From<borrow::Cow<'s, str>> for ScalarCow<'s> {
-    fn from(s: borrow::Cow<'s, str>) -> Self {
-        ScalarCow {
-            0: ScalarCowEnum::Str(s),
-        }
-    }
-}
-
 impl<'s> PartialEq<ScalarCow<'s>> for ScalarCow<'s> {
     fn eq(&self, other: &Self) -> bool {
         scalar_eq(self, other)
@@ -281,6 +326,41 @@ impl<'s> PartialEq<str> for ScalarCow<'s> {
     }
 }
 
+impl<'s> PartialEq<&'s str> for ScalarCow<'s> {
+    fn eq(&self, other: &&str) -> bool {
+        let other = (*other).into();
+        scalar_eq(self, &other)
+    }
+}
+
+impl<'s> PartialEq<String> for ScalarCow<'s> {
+    fn eq(&self, other: &String) -> bool {
+        let other = other.into();
+        scalar_eq(self, &other)
+    }
+}
+
+impl<'s> PartialEq<SString> for ScalarCow<'s> {
+    fn eq(&self, other: &SString) -> bool {
+        let other = other.into();
+        scalar_eq(self, &other)
+    }
+}
+
+impl<'s> PartialEq<SStringRef<'s>> for ScalarCow<'s> {
+    fn eq(&self, other: &SStringRef<'s>) -> bool {
+        let other = other.into();
+        scalar_eq(self, &other)
+    }
+}
+
+impl<'s> PartialEq<SStringCow<'s>> for ScalarCow<'s> {
+    fn eq(&self, other: &SStringCow<'s>) -> bool {
+        let other = other.into();
+        scalar_eq(self, &other)
+    }
+}
+
 impl<'s> Eq for ScalarCow<'s> {}
 
 impl<'s> PartialOrd<ScalarCow<'s>> for ScalarCow<'s> {
@@ -409,28 +489,28 @@ mod test {
 
     #[test]
     fn test_to_str_bool() {
-        assert_eq!(TRUE.to_str(), "true");
+        assert_eq!(TRUE.to_sstr(), "true");
     }
 
     #[test]
     fn test_to_str_integer() {
         let val: ScalarCow<'_> = 42i32.into();
-        assert_eq!(val.to_str(), "42");
+        assert_eq!(val.to_sstr(), "42");
     }
 
     #[test]
     fn test_to_str_float() {
         let val: ScalarCow<'_> = 42f64.into();
-        assert_eq!(val.to_str(), "42");
+        assert_eq!(val.to_sstr(), "42");
 
         let val: ScalarCow<'_> = 42.34.into();
-        assert_eq!(val.to_str(), "42.34");
+        assert_eq!(val.to_sstr(), "42.34");
     }
 
     #[test]
     fn test_to_str_str() {
         let val: ScalarCow<'_> = "foobar".into();
-        assert_eq!(val.to_str(), "foobar");
+        assert_eq!(val.to_sstr(), "foobar");
     }
 
     #[test]
diff --git a/crates/value/src/ser.rs b/crates/value/src/ser.rs
index aeb34f6..80b404e 100644
--- a/crates/value/src/ser.rs
+++ b/crates/value/src/ser.rs
@@ -371,7 +371,7 @@ impl serde::ser::SerializeMap for SerializeMap {
             SerializeMap::Map {
                 ref mut next_key, ..
             } => {
-                *next_key = Some(r#try!(key.serialize(MapKeySerializer)));
+                *next_key = Some(key.serialize(MapKeySerializer)?);
                 Ok(())
             }
         }
@@ -596,7 +596,7 @@ impl serde::ser::SerializeStruct for SerializeMap {
     {
         match *self {
             SerializeMap::Map { .. } => {
-                r#try!(serde::ser::SerializeMap::serialize_key(self, key));
+                serde::ser::SerializeMap::serialize_key(self, key)?;
                 serde::ser::SerializeMap::serialize_value(self, value)
             }
         }
diff --git a/crates/value/src/values.rs b/crates/value/src/values.rs
index 7bb4cdb..f90dfa6 100644
--- a/crates/value/src/values.rs
+++ b/crates/value/src/values.rs
@@ -1,10 +1,11 @@
-use std::borrow;
 use std::cmp::Ordering;
 use std::fmt;
 
-use super::map;
-use super::Scalar;
-use super::ScalarCow;
+use sstring::SStringCow;
+
+use crate::map;
+use crate::Scalar;
+use crate::ScalarCow;
 
 /// An enum to represent different value types
 pub type Value = ValueCow<'static>;
@@ -56,20 +57,18 @@ impl<'v> ValueCow<'v> {
     }
 
     /// Interpret as a string.
-    pub fn to_str(&self) -> borrow::Cow<'_, str> {
-        match *self {
-            ValueCow::Scalar(ref x) => x.to_str(),
-            ValueCow::Array(_) | ValueCow::Object(_) => {
-                borrow::Cow::Owned(self.render().to_string())
-            }
-            ValueCow::Nil | ValueCow::Empty | ValueCow::Blank => borrow::Cow::Borrowed(""),
+    pub fn to_sstr(&self) -> SStringCow<'_> {
+        match self {
+            ValueCow::Scalar(x) => x.to_sstr(),
+            ValueCow::Array(_) | ValueCow::Object(_) => self.render().to_string().into(),
+            ValueCow::Nil | ValueCow::Empty | ValueCow::Blank => SStringCow::default(),
         }
     }
 
     /// Extracts the scalar value if it is a scalar.
     pub fn as_scalar(&self) -> Option<&ScalarCow<'v>> {
-        match *self {
-            ValueCow::Scalar(ref s) => Some(s),
+        match self {
+            ValueCow::Scalar(s) => Some(s),
             _ => None,
         }
     }
@@ -89,7 +88,7 @@ impl<'v> ValueCow<'v> {
 
     /// Extracts the array value if it is an array.
     pub fn as_array(&self) -> Option<&Array> {
-        match *self {
+        match self {
             ValueCow::Array(ref s) => Some(s),
             _ => None,
         }
@@ -110,7 +109,7 @@ impl<'v> ValueCow<'v> {
 
     /// Extracts the object value if it is a object.
     pub fn as_object(&self) -> Option<&Object> {
-        match *self {
+        match self {
             ValueCow::Object(ref s) => Some(s),
             _ => None,
         }
@@ -131,7 +130,7 @@ impl<'v> ValueCow<'v> {
 
     /// Tests whether this value is Nil
     pub fn is_nil(&self) -> bool {
-        match *self {
+        match self {
             ValueCow::Nil => true,
             _ => false,
         }
@@ -139,7 +138,7 @@ impl<'v> ValueCow<'v> {
 
     /// Tests whether this value is Empty
     pub fn is_empty(&self) -> bool {
-        match *self {
+        match self {
             ValueCow::Empty => true,
             _ => false,
         }
@@ -147,7 +146,7 @@ impl<'v> ValueCow<'v> {
 
     /// Tests whether this value is Blank
     pub fn is_blank(&self) -> bool {
-        match *self {
+        match self {
             ValueCow::Blank => true,
             _ => false,
         }
@@ -156,7 +155,7 @@ impl<'v> ValueCow<'v> {
     /// Evaluate using Liquid "truthiness"
     pub fn is_truthy(&self) -> bool {
         // encode Ruby truthiness: all values except false and nil are true
-        match *self {
+        match self {
             ValueCow::Scalar(ref x) => x.is_truthy(),
             ValueCow::Nil | ValueCow::Empty | ValueCow::Blank => false,
             ValueCow::Array(_) | ValueCow::Object(_) => true,
@@ -165,7 +164,7 @@ impl<'v> ValueCow<'v> {
 
     /// Whether a default constructed value.
     pub fn is_default(&self) -> bool {
-        match *self {
+        match self {
             ValueCow::Scalar(ref x) => x.is_default(),
             ValueCow::Nil => true,
             ValueCow::Empty => true,
@@ -177,7 +176,7 @@ impl<'v> ValueCow<'v> {
 
     /// Report the data type (generally for error reporting).
     pub fn type_name(&self) -> &'static str {
-        match *self {
+        match self {
             ValueCow::Scalar(ref x) => x.type_name(),
             ValueCow::Nil => "nil",
             ValueCow::Empty => "empty",
@@ -189,39 +188,33 @@ impl<'v> ValueCow<'v> {
 
     /// Access a contained `Value`.
     pub fn contains_key(&self, index: &ScalarCow<'_>) -> bool {
-        match *self {
+        match self {
             ValueCow::Array(ref x) => {
                 if let Some(index) = index.to_integer() {
                     let index = convert_index(index, x.len());
                     index < x.len()
                 } else {
-                    match &*index.to_str() {
+                    match &*index.to_sstr() {
                         "first" | "last" => true,
                         _ => false,
                     }
                 }
             }
-            ValueCow::Object(ref x) => x.contains_key(index.to_str().as_ref()),
+            ValueCow::Object(ref x) => x.contains_key(index.to_sstr().as_str()),
             _ => false,
         }
     }
 
     /// Keys available for lookup.
-    pub fn keys(&self) -> Keys {
-        let v = match *self {
+    pub fn keys(&self) -> Keys<'_> {
+        let v = match self {
             ValueCow::Array(ref x) => {
                 let start: i32 = 0;
                 let end = x.len() as i32;
                 let keys: Vec<_> = (start..end).map(Scalar::new).collect();
                 keys
             }
-            ValueCow::Object(ref x) => x
-                .keys()
-                .map(|s| match *s {
-                    borrow::Cow::Borrowed(s) => Scalar::new(s),
-                    borrow::Cow::Owned(ref s) => Scalar::new(s.to_owned()),
-                })
-                .collect(),
+            ValueCow::Object(x) => x.keys().map(|s| ScalarCow::new(s)).collect(),
             _ => vec![],
         };
         Keys(v.into_iter())
@@ -229,20 +222,20 @@ impl<'v> ValueCow<'v> {
 
     /// Access a contained `Value`.
     pub fn get<'s>(&'s self, index: &ScalarCow<'_>) -> Option<&'s Self> {
-        match *self {
+        match self {
             ValueCow::Array(ref x) => {
                 if let Some(index) = index.to_integer() {
                     let index = convert_index(index, x.len());
                     x.get(index as usize)
                 } else {
-                    match &*index.to_str() {
+                    match &*index.to_sstr() {
                         "first" => x.get(0),
                         "last" => x.get(x.len() - 1),
                         _ => None,
                     }
                 }
             }
-            ValueCow::Object(ref x) => x.get(index.to_str().as_ref()),
+            ValueCow::Object(ref x) => x.get(index.to_sstr().as_str()),
             _ => None,
         }
     }
@@ -250,13 +243,13 @@ impl<'v> ValueCow<'v> {
 
 /// Iterator over a `Value`s keys.
 #[derive(Debug)]
-pub struct Keys(::std::vec::IntoIter<Scalar>);
+pub struct Keys<'s>(::std::vec::IntoIter<ScalarCow<'s>>);
 
-impl Iterator for Keys {
-    type Item = Scalar;
+impl<'s> Iterator for Keys<'s> {
+    type Item = ScalarCow<'s>;
 
     #[inline]
-    fn next(&mut self) -> Option<Scalar> {
+    fn next(&mut self) -> Option<ScalarCow<'s>> {
         self.0.next()
     }
 
@@ -271,7 +264,7 @@ impl Iterator for Keys {
     }
 }
 
-impl ExactSizeIterator for Keys {
+impl<'s> ExactSizeIterator for Keys<'s> {
     #[inline]
     fn len(&self) -> usize {
         self.0.len()
@@ -297,6 +290,72 @@ impl PartialEq<Value> for Value {
     }
 }
 
+impl PartialEq<i32> for Value {
+    fn eq(&self, other: &i32) -> bool {
+        value_eq(self, &ValueCow::scalar(*other))
+    }
+}
+
+impl PartialEq<f64> for Value {
+    fn eq(&self, other: &f64) -> bool {
+        value_eq(self, &ValueCow::scalar(*other))
+    }
+}
+
+impl PartialEq<bool> for Value {
+    fn eq(&self, other: &bool) -> bool {
+        value_eq(self, &ValueCow::scalar(*other))
+    }
+}
+
+impl PartialEq<crate::DateTime> for Value {
+    fn eq(&self, other: &crate::DateTime) -> bool {
+        value_eq(self, &ValueCow::scalar(*other))
+    }
+}
+
+impl PartialEq<crate::Date> for Value {
+    fn eq(&self, other: &crate::Date) -> bool {
+        value_eq(self, &ValueCow::scalar(*other))
+    }
+}
+
+impl PartialEq<str> for Value {
+    fn eq(&self, other: &str) -> bool {
+        value_eq(self, &ValueCow::scalar(other))
+    }
+}
+
+impl<'s> PartialEq<&'s str> for Value {
+    fn eq(&self, other: &&str) -> bool {
+        value_eq(self, &ValueCow::scalar(*other))
+    }
+}
+
+impl<'s> PartialEq<String> for Value {
+    fn eq(&self, other: &String) -> bool {
+        value_eq(self, &ValueCow::scalar(other.as_str()))
+    }
+}
+
+impl PartialEq<sstring::SString> for Value {
+    fn eq(&self, other: &sstring::SString) -> bool {
+        value_eq(self, &ValueCow::scalar(other))
+    }
+}
+
+impl<'s> PartialEq<sstring::SStringRef<'s>> for Value {
+    fn eq(&self, other: &sstring::SStringRef<'s>) -> bool {
+        value_eq(self, &ValueCow::scalar(other))
+    }
+}
+
+impl<'s> PartialEq<sstring::SStringCow<'s>> for Value {
+    fn eq(&self, other: &sstring::SStringCow<'s>) -> bool {
+        value_eq(self, &ValueCow::scalar(other))
+    }
+}
+
 impl Eq for Value {}
 
 impl PartialOrd<Value> for Value {
@@ -360,7 +419,7 @@ impl<'s> fmt::Display for ValueRendered<'s> {
     }
 }
 
-fn value_eq(lhs: &Value, rhs: &Value) -> bool {
+fn value_eq<'v>(lhs: &'v ValueCow<'v>, rhs: &'v ValueCow<'v>) -> bool {
     match (lhs, rhs) {
         (&ValueCow::Scalar(ref x), &ValueCow::Scalar(ref y)) => x == y,
         (&ValueCow::Array(ref x), &ValueCow::Array(ref y)) => x == y,
@@ -375,7 +434,7 @@ fn value_eq(lhs: &Value, rhs: &Value) -> bool {
         | (&ValueCow::Blank, &ValueCow::Empty) => true,
 
         (&ValueCow::Empty, &ValueCow::Scalar(ref s))
-        | (&ValueCow::Scalar(ref s), &ValueCow::Empty) => s.to_str().is_empty(),
+        | (&ValueCow::Scalar(ref s), &ValueCow::Empty) => s.to_sstr().is_empty(),
         (&ValueCow::Empty, &ValueCow::Array(ref s))
         | (&ValueCow::Array(ref s), &ValueCow::Empty) => s.is_empty(),
         (&ValueCow::Empty, &ValueCow::Object(ref s))
@@ -384,7 +443,7 @@ fn value_eq(lhs: &Value, rhs: &Value) -> bool {
         (&ValueCow::Nil, &ValueCow::Blank) | (&ValueCow::Blank, &ValueCow::Nil) => true,
         (&ValueCow::Blank, &ValueCow::Scalar(ref s))
         | (&ValueCow::Scalar(ref s), &ValueCow::Blank) => {
-            s.to_str().trim().is_empty() || !s.to_bool().unwrap_or(true)
+            s.to_sstr().trim().is_empty() || !s.to_bool().unwrap_or(true)
         }
         (&ValueCow::Blank, &ValueCow::Array(ref s))
         | (&ValueCow::Array(ref s), &ValueCow::Blank) => s.is_empty(),
@@ -403,7 +462,7 @@ fn value_eq(lhs: &Value, rhs: &Value) -> bool {
     }
 }
 
-fn value_cmp(lhs: &Value, rhs: &Value) -> Option<Ordering> {
+fn value_cmp(lhs: &ValueCow, rhs: &ValueCow) -> Option<Ordering> {
     match (lhs, rhs) {
         (&ValueCow::Scalar(ref x), &ValueCow::Scalar(ref y)) => x.partial_cmp(y),
         (&ValueCow::Array(ref x), &ValueCow::Array(ref y)) => x.iter().partial_cmp(y.iter()),
@@ -420,7 +479,7 @@ mod test {
     fn test_to_string_scalar() {
         let val = ValueCow::scalar(42f64);
         assert_eq!(&val.render().to_string(), "42");
-        assert_eq!(&val.to_str(), "42");
+        assert_eq!(&val.to_sstr(), "42");
     }
 
     #[test]
@@ -431,7 +490,7 @@ mod test {
             ValueCow::scalar(5.3),
         ]);
         assert_eq!(&val.render().to_string(), "3test5.3");
-        assert_eq!(&val.to_str(), "3test5.3");
+        assert_eq!(&val.to_sstr(), "3test5.3");
     }
 
     // TODO make a test for object, remember values are in arbitrary orders in HashMaps
@@ -439,7 +498,7 @@ mod test {
     #[test]
     fn test_to_string_nil() {
         assert_eq!(&ValueCow::Nil.render().to_string(), "");
-        assert_eq!(&ValueCow::Nil.to_str(), "");
+        assert_eq!(&ValueCow::Nil.to_sstr(), "");
     }
 
     #[test]
diff --git a/src/filters/extra/date.rs b/src/filters/extra/date.rs
index e0c171d..01047ee 100644
--- a/src/filters/extra/date.rs
+++ b/src/filters/extra/date.rs
@@ -47,7 +47,7 @@ impl Filter for DateInTzFilter {
 
         let timezone = FixedOffset::east(args.timezone * 3600);
 
-        let formatter = date.with_timezone(&timezone).format(args.format.as_ref());
+        let formatter = date.with_timezone(&timezone).format(args.format.as_str());
         let date = formatter.to_string();
         Ok(Value::scalar(date))
     }
diff --git a/src/filters/jekyll/array.rs b/src/filters/jekyll/array.rs
index 2f2fb2f..41c9a1d 100644
--- a/src/filters/jekyll/array.rs
+++ b/src/filters/jekyll/array.rs
@@ -170,7 +170,7 @@ impl Filter for ArrayToSentenceStringFilter {
 
         let mut sentence = array
             .next()
-            .map(|v| v.to_str().into_owned())
+            .map(|v| v.to_sstr().into_mut())
             .unwrap_or_else(|| "".to_string());
 
         let last = array.next_back();
diff --git a/src/filters/jekyll/slugify.rs b/src/filters/jekyll/slugify.rs
index 6989b59..c0849f0 100644
--- a/src/filters/jekyll/slugify.rs
+++ b/src/filters/jekyll/slugify.rs
@@ -69,10 +69,10 @@ impl Filter for SlugifyFilter {
     fn evaluate(&self, input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
 
-        let s = input.to_str();
+        let s = input.to_sstr();
         let mode = args
             .mode
-            .map(|mode| SlugifyMode::new(mode.as_ref()))
+            .map(|mode| SlugifyMode::new(mode.as_str()))
             .unwrap_or(SlugifyMode::Def);
 
         let s = if mode == SlugifyMode::Latin {
diff --git a/src/filters/mod.rs b/src/filters/mod.rs
index f7d92b6..43cc1b3 100644
--- a/src/filters/mod.rs
+++ b/src/filters/mod.rs
@@ -1,5 +1,4 @@
 use liquid_error::Error;
-use std::borrow::Cow;
 
 pub mod std;
 
@@ -11,14 +10,14 @@ pub mod extra;
 
 pub fn invalid_input<S>(cause: S) -> Error
 where
-    S: Into<Cow<'static, str>>,
+    S: Into<sstring::SString>,
 {
     Error::with_msg("Invalid input").context("cause", cause)
 }
 
 pub fn invalid_argument<S>(argument: S, cause: S) -> Error
 where
-    S: Into<Cow<'static, str>>,
+    S: Into<sstring::SString>,
 {
     Error::with_msg("Invalid argument")
         .context("argument", argument)
diff --git a/src/filters/std/array.rs b/src/filters/std/array.rs
index 3db0f23..74b2650 100644
--- a/src/filters/std/array.rs
+++ b/src/filters/std/array.rs
@@ -52,9 +52,9 @@ impl Filter for JoinFilter {
         let input = input
             .as_array()
             .ok_or_else(|| invalid_input("Array of strings expected"))?;
-        let input = input.iter().map(|x| x.to_str());
+        let input = input.iter().map(|x| x.to_sstr());
 
-        Ok(Value::scalar(itertools::join(input, separator.as_ref())))
+        Ok(Value::scalar(itertools::join(input, separator.as_str())))
     }
 }
 
@@ -70,7 +70,7 @@ fn nil_safe_compare(a: &Value, b: &Value) -> Option<cmp::Ordering> {
 fn nil_safe_casecmp_key(value: &Value) -> Option<String> {
     match value {
         Value::Nil => None,
-        value => Some(value.to_str().to_lowercase()),
+        value => Some(value.to_sstr().to_lowercase()),
     }
 }
 
@@ -387,7 +387,7 @@ impl Filter for CompactFilter {
                 .iter()
                 .filter(|v| {
                     !v.as_object()
-                        .and_then(|obj| obj.get(property.as_ref()))
+                        .and_then(|obj| obj.get(property.as_str()))
                         .map_or(true, Value::is_nil)
                 })
                 .cloned()
@@ -460,7 +460,7 @@ impl Filter for FirstFilter {
         match *input {
             Value::Scalar(ref x) => {
                 let c = x
-                    .to_str()
+                    .to_sstr()
                     .chars()
                     .next()
                     .map(|c| c.to_string())
@@ -490,7 +490,7 @@ impl Filter for LastFilter {
         match *input {
             Value::Scalar(ref x) => {
                 let c = x
-                    .to_str()
+                    .to_sstr()
                     .chars()
                     .last()
                     .map(|c| c.to_string())
diff --git a/src/filters/std/date.rs b/src/filters/std/date.rs
index ff06531..a39bf0a 100644
--- a/src/filters/std/date.rs
+++ b/src/filters/std/date.rs
@@ -34,7 +34,7 @@ impl Filter for DateFilter {
         let date = input.as_scalar().and_then(Scalar::to_date_time);
         match date {
             Some(date) if !args.format.is_empty() => {
-                Ok(Value::scalar(date.format(args.format.as_ref()).to_string()))
+                Ok(Value::scalar(date.format(args.format.as_str()).to_string()))
             }
             _ => Ok(input.clone()),
         }
diff --git a/src/filters/std/html.rs b/src/filters/std/html.rs
index 68114ec..6484c74 100644
--- a/src/filters/std/html.rs
+++ b/src/filters/std/html.rs
@@ -22,7 +22,7 @@ fn escape(input: &Value, once_p: bool) -> Result<Value> {
     if input.is_nil() {
         return Ok(Value::Nil);
     }
-    let s = input.to_str();
+    let s = input.to_sstr();
     let mut result = String::new();
     let mut last = 0;
     let mut skip = 0;
@@ -122,7 +122,7 @@ static MATCHERS: once_cell::sync::Lazy<[Regex; 4]> = once_cell::sync::Lazy::new(
 
 impl Filter for StripHtmlFilter {
     fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
-        let input = input.to_str().into_owned();
+        let input = input.to_sstr().into_mut();
 
         let result = MATCHERS.iter().fold(input, |acc, matcher| {
             matcher.replace_all(&acc, "").into_owned()
@@ -146,7 +146,7 @@ struct NewlineToBrFilter;
 impl Filter for NewlineToBrFilter {
     fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
         // TODO handle windows line endings
-        let input = input.to_str();
+        let input = input.to_sstr();
         Ok(Value::scalar(input.replace("\n", "<br />\n")))
     }
 }
diff --git a/src/filters/std/mod.rs b/src/filters/std/mod.rs
index d67bfac..cafdd42 100644
--- a/src/filters/std/mod.rs
+++ b/src/filters/std/mod.rs
@@ -44,7 +44,7 @@ struct SizeFilter;
 impl Filter for SizeFilter {
     fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
         match *input {
-            Value::Scalar(ref x) => Ok(Value::scalar(x.to_str().len() as i32)),
+            Value::Scalar(ref x) => Ok(Value::scalar(x.to_sstr().len() as i32)),
             Value::Array(ref x) => Ok(Value::scalar(x.len() as i32)),
             Value::Object(ref x) => Ok(Value::scalar(x.len() as i32)),
             _ => Ok(Value::scalar(0i32)),
diff --git a/src/filters/std/slice.rs b/src/filters/std/slice.rs
index a214aa8..177b82d 100644
--- a/src/filters/std/slice.rs
+++ b/src/filters/std/slice.rs
@@ -75,7 +75,7 @@ impl Filter for SliceFilter {
                 input.iter().skip(offset).take(length).cloned(),
             ))
         } else {
-            let input = input.to_str();
+            let input = input.to_sstr();
             let (offset, length) = canonicalize_slice(offset, length, input.len());
             Ok(Value::scalar(
                 input.chars().skip(offset).take(length).collect::<String>(),
diff --git a/src/filters/std/string/case.rs b/src/filters/std/string/case.rs
index 0b00885..88d69fd 100644
--- a/src/filters/std/string/case.rs
+++ b/src/filters/std/string/case.rs
@@ -18,7 +18,7 @@ struct DowncaseFilter;
 
 impl Filter for DowncaseFilter {
     fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
-        let s = input.to_str();
+        let s = input.to_sstr();
         Ok(Value::scalar(s.to_lowercase()))
     }
 }
@@ -37,7 +37,7 @@ struct UpcaseFilter;
 
 impl Filter for UpcaseFilter {
     fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
-        let s = input.to_str();
+        let s = input.to_sstr();
         Ok(Value::scalar(s.to_uppercase()))
     }
 }
@@ -56,7 +56,7 @@ struct CapitalizeFilter;
 
 impl Filter for CapitalizeFilter {
     fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
-        let s = input.to_str().to_owned();
+        let s = input.to_sstr().to_owned();
         let mut chars = s.chars();
         let capitalized = match chars.next() {
             Some(first_char) => first_char.to_uppercase().chain(chars).collect(),
diff --git a/src/filters/std/string/mod.rs b/src/filters/std/string/mod.rs
index 72c2358..42c7b17 100644
--- a/src/filters/std/string/mod.rs
+++ b/src/filters/std/string/mod.rs
@@ -39,12 +39,12 @@ impl Filter for SplitFilter {
     fn evaluate(&self, input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
 
-        let input = input.to_str();
+        let input = input.to_sstr();
 
         // Split and construct resulting Array
         Ok(Value::Array(
             input
-                .split(args.pattern.as_ref())
+                .split(args.pattern.as_str())
                 .map(|s| Value::scalar(s.to_owned()))
                 .collect(),
         ))
diff --git a/src/filters/std/string/operate.rs b/src/filters/std/string/operate.rs
index 0691fea..6c0895c 100644
--- a/src/filters/std/string/operate.rs
+++ b/src/filters/std/string/operate.rs
@@ -36,12 +36,12 @@ impl Filter for ReplaceFilter {
     fn evaluate(&self, input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
 
-        let input = input.to_str();
+        let input = input.to_sstr();
 
         let replace = args.replace.unwrap_or_else(|| "".into());
 
         Ok(Value::scalar(
-            input.replace(args.search.as_ref(), replace.as_ref()),
+            input.replace(args.search.as_str(), replace.as_str()),
         ))
     }
 }
@@ -77,15 +77,15 @@ impl Filter for ReplaceFirstFilter {
     fn evaluate(&self, input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
 
-        let input = input.to_str();
+        let input = input.to_sstr();
 
         let search = args.search;
         let replace = args.replace.unwrap_or_else(|| "".into());
 
         {
-            let tokens: Vec<&str> = input.splitn(2, search.as_ref()).collect();
+            let tokens: Vec<&str> = input.splitn(2, search.as_str()).collect();
             if tokens.len() == 2 {
-                let result = [tokens[0], replace.as_ref(), tokens[1]].join("");
+                let result = [tokens[0], replace.as_str(), tokens[1]].join("");
                 return Ok(Value::scalar(result));
             }
         }
@@ -119,9 +119,9 @@ impl Filter for RemoveFilter {
     fn evaluate(&self, input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
 
-        let input = input.to_str();
+        let input = input.to_sstr();
 
-        Ok(Value::scalar(input.replace(args.search.as_ref(), "")))
+        Ok(Value::scalar(input.replace(args.search.as_str(), "")))
     }
 }
 
@@ -151,10 +151,10 @@ impl Filter for RemoveFirstFilter {
     fn evaluate(&self, input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
 
-        let input = input.to_str();
+        let input = input.to_sstr();
 
         Ok(Value::scalar(
-            input.splitn(2, args.search.as_ref()).collect::<String>(),
+            input.splitn(2, args.search.as_str()).collect::<String>(),
         ))
     }
 }
@@ -185,8 +185,8 @@ impl Filter for AppendFilter {
     fn evaluate(&self, input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
 
-        let mut input = input.to_str().into_owned();
-        input.push_str(args.string.as_ref());
+        let mut input = input.to_sstr().into_mut();
+        input.push_str(args.string.as_str());
 
         Ok(Value::scalar(input))
     }
@@ -218,9 +218,9 @@ impl Filter for PrependFilter {
     fn evaluate(&self, input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
 
-        let input = input.to_str();
-        let mut string = args.string.into_owned();
-        string.push_str(input.as_ref());
+        let input = input.to_sstr();
+        let mut string = args.string.into_mut();
+        string.push_str(input.as_str());
 
         Ok(Value::scalar(string))
     }
diff --git a/src/filters/std/string/strip.rs b/src/filters/std/string/strip.rs
index cfe8e59..b2e0b0f 100644
--- a/src/filters/std/string/strip.rs
+++ b/src/filters/std/string/strip.rs
@@ -25,7 +25,7 @@ struct StripFilter;
 
 impl Filter for StripFilter {
     fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
-        let input = input.to_str();
+        let input = input.to_sstr();
         Ok(Value::scalar(input.trim().to_owned()))
     }
 }
@@ -50,7 +50,7 @@ struct LstripFilter;
 
 impl Filter for LstripFilter {
     fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
-        let input = input.to_str();
+        let input = input.to_sstr();
         Ok(Value::scalar(input.trim_start().to_owned()))
     }
 }
@@ -75,7 +75,7 @@ struct RstripFilter;
 
 impl Filter for RstripFilter {
     fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
-        let input = input.to_str();
+        let input = input.to_sstr();
         Ok(Value::scalar(input.trim_end().to_owned()))
     }
 }
@@ -94,7 +94,7 @@ struct StripNewlinesFilter;
 
 impl Filter for StripNewlinesFilter {
     fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
-        let input = input.to_str();
+        let input = input.to_sstr();
         Ok(Value::scalar(
             input
                 .chars()
diff --git a/src/filters/std/string/truncate.rs b/src/filters/std/string/truncate.rs
index 4eb0962..db13bf2 100644
--- a/src/filters/std/string/truncate.rs
+++ b/src/filters/std/string/truncate.rs
@@ -75,15 +75,15 @@ impl Filter for TruncateFilter {
 
         let l = cmp::max(lenght - truncate_string.len(), 0);
 
-        let input_string = input.to_str();
+        let input_string = input.to_sstr();
 
         let result = if lenght < input_string.len() {
-            let result = UnicodeSegmentation::graphemes(input_string.as_ref(), true)
+            let result = UnicodeSegmentation::graphemes(input_string.as_str(), true)
                 .take(l)
                 .collect::<Vec<&str>>()
                 .join("")
                 .to_string()
-                + truncate_string.as_ref();
+                + truncate_string.as_str();
             Value::scalar(result)
         } else {
             input.clone()
@@ -133,11 +133,11 @@ impl Filter for TruncateWordsFilter {
 
         let l = cmp::max(words, 0);
 
-        let input_string = input.to_str();
+        let input_string = input.to_sstr();
 
         let word_list: Vec<&str> = input_string.split(' ').collect();
         let result = if words < word_list.len() {
-            let result = itertools::join(word_list.iter().take(l), " ") + truncate_string.as_ref();
+            let result = itertools::join(word_list.iter().take(l), " ") + truncate_string.as_str();
             Value::scalar(result)
         } else {
             input.clone()
diff --git a/src/filters/std/url.rs b/src/filters/std/url.rs
index 4cf71ce..f7f86c6 100644
--- a/src/filters/std/url.rs
+++ b/src/filters/std/url.rs
@@ -53,10 +53,10 @@ impl Filter for UrlEncodeFilter {
             return Ok(Value::Nil);
         }
 
-        let s = input.to_str();
+        let s = input.to_sstr();
 
         let result: String =
-            percent_encoding::utf8_percent_encode(s.as_ref(), URL_ENCODE_SET.clone()).collect();
+            percent_encoding::utf8_percent_encode(s.as_str(), URL_ENCODE_SET.clone()).collect();
         Ok(Value::scalar(result))
     }
 }
@@ -79,7 +79,7 @@ impl Filter for UrlDecodeFilter {
             return Ok(Value::Nil);
         }
 
-        let s = input.to_str().replace("+", " ");
+        let s = input.to_sstr().replace("+", " ");
 
         let result = percent_encoding::percent_decode(s.as_bytes())
             .decode_utf8()
diff --git a/src/tags/case_block.rs b/src/tags/case_block.rs
index ea80dc4..d05b6ab 100644
--- a/src/tags/case_block.rs
+++ b/src/tags/case_block.rs
@@ -66,7 +66,7 @@ impl Renderable for Case {
                     .trace_with(|| case.trace().into())
                     .trace_with(|| self.trace().into())
                     .context_key_with(|| self.target.to_string().into())
-                    .value_with(|| value.to_str().into_owned().into());
+                    .value_with(|| value.to_sstr().into_owned());
             }
         }
 
@@ -76,7 +76,7 @@ impl Renderable for Case {
                 .trace("{{% else %}}")
                 .trace_with(|| self.trace().into())
                 .context_key_with(|| self.target.to_string().into())
-                .value_with(|| value.to_str().into_owned().into());
+                .value_with(|| value.to_sstr().into_owned());
         }
 
         Ok(())
diff --git a/src/tags/cycle_tag.rs b/src/tags/cycle_tag.rs
index 90a79d5..be1e92b 100644
--- a/src/tags/cycle_tag.rs
+++ b/src/tags/cycle_tag.rs
@@ -55,7 +55,7 @@ fn parse_cycle(mut arguments: TagTokenIter, _options: &Language) -> Result<Cycle
                 TryMatchToken::Fails(name) => match name.expect_literal() {
                     // This will allow non string literals such as 0 to be parsed as such.
                     // Is this ok or should more specific functions be created?
-                    TryMatchToken::Matches(name) => name.to_str().into_owned(),
+                    TryMatchToken::Matches(name) => name.to_sstr().into_mut(),
                     TryMatchToken::Fails(name) => return name.raise_error().into_err(),
                 },
             };
diff --git a/src/tags/for_block.rs b/src/tags/for_block.rs
index b0e8d2e..1752e40 100644
--- a/src/tags/for_block.rs
+++ b/src/tags/for_block.rs
@@ -145,7 +145,7 @@ fn int_argument(arg: &Expression, context: &Context, arg_name: &str) -> Result<i
         .and_then(Scalar::to_integer)
         .ok_or_else(|| unexpected_value_error("whole number", Some(value.type_name())))
         .context_key_with(|| arg_name.to_owned().into())
-        .value_with(|| value.to_str().into_owned().into())?;
+        .value_with(|| value.to_sstr().into_owned())?;
 
     Ok(value as isize)
 }
@@ -873,7 +873,7 @@ mod test {
 
     impl Filter for ShoutFilter {
         fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
-            Ok(Value::scalar(input.to_str().to_uppercase()))
+            Ok(Value::scalar(input.to_sstr().to_uppercase()))
         }
     }
 
diff --git a/src/tags/if_block.rs b/src/tags/if_block.rs
index 1202706..ba43ec1 100644
--- a/src/tags/if_block.rs
+++ b/src/tags/if_block.rs
@@ -153,8 +153,8 @@ struct Conditional {
 fn contains_check(a: &Value, b: &Value) -> Result<bool> {
     match *a {
         Value::Scalar(ref val) => {
-            let b = b.to_str();
-            Ok(val.to_str().contains(b.as_ref()))
+            let b = b.to_sstr();
+            Ok(val.to_sstr().contains(b.as_str()))
         }
         Value::Object(_) => {
             let b = b.as_scalar();
diff --git a/src/tags/include_tag.rs b/src/tags/include_tag.rs
index a9d34e2..40d2b7b 100644
--- a/src/tags/include_tag.rs
+++ b/src/tags/include_tag.rs
@@ -65,8 +65,8 @@ impl ParseTag for IncludeTag {
         // This may accept strange inputs such as `{% include 0 %}` or `{% include filterchain | filter:0 %}`.
         // Those inputs would fail anyway by there being not a path with those names so they are not a big concern.
         let name = match name.expect_literal() {
-            // Using `to_str()` on literals ensures `Strings` will have their quotes trimmed.
-            TryMatchToken::Matches(name) => name.to_str().to_string(),
+            // Using `to_sstr()` on literals ensures `Strings` will have their quotes trimmed.
+            TryMatchToken::Matches(name) => name.to_sstr().to_string(),
             TryMatchToken::Fails(name) => name.as_str().to_string(),
         };
 
@@ -145,7 +145,7 @@ mod test {
     impl Filter for SizeFilter {
         fn evaluate(&self, input: &Value, _context: &Context) -> Result<Value> {
             match *input {
-                Value::Scalar(ref x) => Ok(Value::scalar(x.to_str().len() as i32)),
+                Value::Scalar(ref x) => Ok(Value::scalar(x.to_sstr().len() as i32)),
                 Value::Array(ref x) => Ok(Value::scalar(x.len() as i32)),
                 Value::Object(ref x) => Ok(Value::scalar(x.len() as i32)),
                 _ => Ok(Value::scalar(0i32)),


Test patch:
diff --git a/tests/conformance_ruby/output_test.rs b/tests/conformance_ruby/output_test.rs
index 50191c9..9f4ca32 100644
--- a/tests/conformance_ruby/output_test.rs
+++ b/tests/conformance_ruby/output_test.rs
@@ -6,7 +6,6 @@ use liquid::error::Result;
 use liquid::interpreter::Context;
 use liquid::interpreter::Expression;
 use liquid::value::Value;
-use std::borrow::Cow;
 
 #[derive(Clone, ParseFilter, FilterReflection)]
 #[filter(
@@ -69,7 +68,7 @@ pub struct AddSmileyFilter {
 impl Filter for AddSmileyFilter {
     fn evaluate(&self, input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
-        let smiley = args.smiley.unwrap_or(Cow::from(":-)")).to_string();
+        let smiley = args.smiley.unwrap_or(":-)".into()).to_string();
         Ok(Value::scalar(format!("{} {}", input.render(), smiley)))
     }
 }
@@ -103,8 +102,8 @@ impl Filter for AddTagFilter {
     fn evaluate(&self, input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
 
-        let tag = args.tag.unwrap_or(Cow::from("p")).to_string();
-        let id = args.id.unwrap_or(Cow::from("foo")).to_string();
+        let tag = args.tag.unwrap_or("p".into()).to_string();
+        let id = args.id.unwrap_or("foo".into()).to_string();
         Ok(Value::scalar(format!(
             r#"<{} id="{}">{}</{}>"#,
             tag,
@@ -160,7 +159,7 @@ impl Filter for LinkToFilter {
         let args = self.args.evaluate(context)?;
 
         let name = input;
-        let url = args.url.unwrap_or(Cow::from(":-)")).to_string();
+        let url = args.url.unwrap_or(":-)".into()).to_string();
         Ok(Value::scalar(format!(
             r#"<a href="{}">{}</a>"#,
             url,
diff --git a/tests/derive_macros_test_filters/mixed.rs b/tests/derive_macros_test_filters/mixed.rs
index ad9ee2a..26ce9ff 100644
--- a/tests/derive_macros_test_filters/mixed.rs
+++ b/tests/derive_macros_test_filters/mixed.rs
@@ -61,7 +61,7 @@ impl Filter for TestMixedFilter {
         let d = args.d.to_string();
         let e = args.e.to_string();
         let f = args.f.map(|i| i.to_string()).unwrap_or("None".to_string());
-        let g = args.g.to_str();
+        let g = args.g.to_sstr();
 
         let result = format!(
             "<a: {}; b: {}; c: {}, d: {}, e: {}, f: {}, type: {}>",
diff --git a/tests/derive_macros_test_filters/positional.rs b/tests/derive_macros_test_filters/positional.rs
index b07c949..d74239e 100644
--- a/tests/derive_macros_test_filters/positional.rs
+++ b/tests/derive_macros_test_filters/positional.rs
@@ -38,7 +38,7 @@ impl Filter for TestPositionalFilter {
     fn evaluate(&self, _input: &Value, context: &Context) -> Result<Value> {
         let args = self.args.evaluate(context)?;
 
-        let pos1 = args.pos1.to_str();
+        let pos1 = args.pos1.to_sstr();
         let result = if let Some(pos2) = args.pos2 {
             format!("<pos1: {}; pos2: {}>", pos1, pos2)
         } else {