diff --git a/Cargo.toml b/Cargo.toml index 15b40e89..4a93965f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "resource", "rio", "sophia", + "term", "turtle", #"jsonld", "xml", @@ -31,6 +32,7 @@ sophia_iri = { version = "0.8.0-alpha.1", path = "./iri" } sophia_isomorphism = { version = "0.8.0-alpha.1", path = "./isomorphism" } sophia_resource = { version = "0.8.0-alpha.1", path = "./resource" } sophia_rio = { version = "0.8.0-alpha.1", path = "./rio" } +sophia_term = { version = "0.8.0-alpha.1", path = "./term" } sophia_turtle = { version = "0.8.0-alpha.1", path = "./turtle" } sophia_xml = { version = "0.8.0-alpha.1", path = "./xml" } diff --git a/README.md b/README.md index 15cf7f8f..f229ebf8 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ It comprises the following crates: - parsers and serializers * [`sophia_iri`] provides functions, types and traits for validating and resolving IRIs. * [`sophia_inmem`] defines in-memory implementations of the `Graph` and `Dataset` traits from `sophia_api`. +* [`sophia_term`] defines various implementations of the `Term` trait from `sophia_api`. * [`sophia_turtle`] provides parsers and serializers for the Turtle-family of concrete syntaxes. * [`sophia_xml`] provides parsers and serializers for RDF/XML. * [`sophia_jsonld`] provides preliminary support for JSON-LD. @@ -73,11 +74,11 @@ An outdated comparison of Sophia with other RDF libraries is still available [`sophia_iri`]: https://crates.io/crates/sophia_iri [`sophia_term`]: https://crates.io/crates/sophia_term [`sophia_inmem`]: https://crates.io/crates/sophia_inmem +[`sophia_term`]: https://crates.io/crates/sophia_inmem [`sophia_turtle`]: https://crates.io/crates/sophia_turtle [`sophia_xml`]: https://crates.io/crates/sophia_xml [`sophia_jsonld`]: https://crates.io/crates/sophia_jsonld [`sophia_c14n`]: https://crates.io/crates/sophia_c14n -[`sophia_indexed`]: https://crates.io/crates/sophia_indexed [`sophia_rio`]: https://crates.io/crates/sophia_rio [`sophia`]: https://crates.io/crates/sophia [CECILL-B]: https://cecill.info/licences/Licence_CeCILL-B_V1-en.html diff --git a/sophia/Cargo.toml b/sophia/Cargo.toml index 31f81728..6fe99b8e 100644 --- a/sophia/Cargo.toml +++ b/sophia/Cargo.toml @@ -29,4 +29,5 @@ sophia_isomorphism.workspace = true sophia_resource.workspace = true sophia_rio.workspace = true sophia_turtle.workspace = true +sophia_term.workspace = true sophia_xml = { workspace = true, optional = true } diff --git a/sophia/src/lib.rs b/sophia/src/lib.rs index 0eafdd9f..475cb694 100644 --- a/sophia/src/lib.rs +++ b/sophia/src/lib.rs @@ -5,10 +5,13 @@ //! that make the Sophia toolkit: //! //! * [`api`] +//! * [`c14n`] //! * [`inmem`] //! * [`iri`] //! * [`isomorphism`] +//! * [`resource`] //! * [`turtle`] +//! * [`term`] //! * [`xml`] (with the `xml` feature enabled) //! //! # Getting Started @@ -57,6 +60,7 @@ pub use sophia_inmem as inmem; pub use sophia_iri as iri; pub use sophia_isomorphism as isomorphism; pub use sophia_resource as resource; +pub use sophia_term as term; pub use sophia_turtle as turtle; #[cfg(feature = "xml")] pub use sophia_xml as xml; diff --git a/term/Cargo.toml b/term/Cargo.toml new file mode 100644 index 00000000..eed63c5f --- /dev/null +++ b/term/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sophia_term" +description = "A Rust toolkit for RDF and Linked Data - In-memory Graph and Dataset implementations" +documentation = "https://docs.rs/sophia_term" +version.workspace = true +authors.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true +keywords.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +# This feature increases the number of tests +all_tests = [] + +[dependencies] +sophia_api.workspace = true +lazy_static.workspace = true + +[dev-dependencies] diff --git a/term/src/_generic.rs b/term/src/_generic.rs new file mode 100644 index 00000000..9ff13829 --- /dev/null +++ b/term/src/_generic.rs @@ -0,0 +1,421 @@ +use std::borrow::Borrow; +use std::fmt::Debug; + +use sophia_api::ns::rdf; +use sophia_api::term::{ + BnodeId, FromTerm, IriRef, LanguageTag, Term, TermKind, TryFromTerm, VarName, +}; +use sophia_api::MownStr; + +lazy_static::lazy_static! { + static ref RDF_LANG_STRING: IriRef> = rdf::langString.iri().unwrap().map_unchecked(Box::from); +} + +/// A generic implementation of [`Term`]. +/// +/// Note that, although it is possible to use `GenericTerm`, +/// it is recommended to use [`sophia_api::term::SimpleTerm`] instead. +#[derive(Clone, Debug)] +pub enum GenericTerm> { + /// A straighforward implementation of [`Term`] as an enum. + /// An [RDF IRI](https://www.w3.org/TR/rdf11-concepts/#section-IRIs) + Iri(IriRef), + /// An RDF [blank node](https://www.w3.org/TR/rdf11-concepts/#section-blank-nodes) + BlankNode(BnodeId), + /// An RDF [literal](https://www.w3.org/TR/rdf11-concepts/#section-Graph-Literal) + Literal(GenericLiteral), + /// An RDF-star [quoted triple](https://www.w3.org/2021/12/rdf-star.html#dfn-quoted) + Triple(Box<[Self; 3]>), + /// A SPARQL or Notation3 variable + Variable(VarName), +} + +impl + Debug> Term for GenericTerm { + type BorrowTerm<'x> = &'x Self where Self: 'x; + + fn kind(&self) -> sophia_api::term::TermKind { + match self { + GenericTerm::Iri(_) => TermKind::Iri, + GenericTerm::BlankNode(_) => TermKind::BlankNode, + GenericTerm::Literal(_) => TermKind::Literal, + GenericTerm::Triple(_) => TermKind::Triple, + GenericTerm::Variable(_) => TermKind::Variable, + } + } + + fn borrow_term(&self) -> Self::BorrowTerm<'_> { + self + } + + fn is_iri(&self) -> bool { + matches!(self, GenericTerm::Iri(..)) + } + + fn is_blank_node(&self) -> bool { + matches!(self, GenericTerm::BlankNode(..)) + } + + fn is_literal(&self) -> bool { + matches!(self, GenericTerm::Literal(..)) + } + + fn is_variable(&self) -> bool { + matches!(self, GenericTerm::Variable(..)) + } + + fn is_atom(&self) -> bool { + !matches!(self, GenericTerm::Triple(..)) + } + + fn is_triple(&self) -> bool { + matches!(self, GenericTerm::Triple(..)) + } + + fn iri(&self) -> Option> { + if let GenericTerm::Iri(iri) = self { + Some( + iri.as_ref() + .map_unchecked(|t| MownStr::from_str(t.borrow())), + ) + } else { + None + } + } + + fn bnode_id(&self) -> Option> { + if let GenericTerm::BlankNode(id) = self { + Some(id.as_ref().map_unchecked(|t| MownStr::from_str(t.borrow()))) + } else { + None + } + } + + fn lexical_form(&self) -> Option { + if let GenericTerm::Literal(lit) = self { + lit.lexical_form() + } else { + None + } + } + + fn datatype(&self) -> Option> { + if let GenericTerm::Literal(lit) = self { + lit.datatype() + } else { + None + } + } + + fn language_tag(&self) -> Option> { + if let GenericTerm::Literal(lit) = self { + lit.language_tag() + } else { + None + } + } + + fn variable(&self) -> Option> { + if let GenericTerm::Variable(name) = self { + Some( + name.as_ref() + .map_unchecked(|t| MownStr::from_str(t.borrow())), + ) + } else { + None + } + } + + fn triple(&self) -> Option<[Self::BorrowTerm<'_>; 3]> { + if let GenericTerm::Triple(spo) = self { + Some([ + spo[0].borrow_term(), + spo[1].borrow_term(), + spo[2].borrow_term(), + ]) + } else { + None + } + } + + fn to_triple(self) -> Option<[Self; 3]> + where + Self: Sized, + { + if let GenericTerm::Triple(spo) = self { + Some(*spo) + } else { + None + } + } +} + +impl + for<'x> From<&'x str>> FromTerm for GenericTerm { + fn from_term(term: U) -> Self { + match term.kind() { + TermKind::Iri => { + // the following is safe because we checked term.kind() + let iri = unsafe { term.iri().unwrap_unchecked() }; + GenericTerm::Iri(iri.map_unchecked(|txt| T::from(&txt))) + } + TermKind::Literal => { + // the following is safe because we checked term.kind() + let lit = unsafe { GenericLiteral::try_from_term(term).unwrap_unchecked() }; + GenericTerm::Literal(lit) + } + TermKind::BlankNode => { + // the following is safe because we checked term.kind() + let id = unsafe { term.bnode_id().unwrap_unchecked() }; + GenericTerm::BlankNode(id.map_unchecked(|txt| T::from(&txt))) + } + TermKind::Triple => { + // the following is safe because we checked term.kind() + let spo = unsafe { term.triple().unwrap_unchecked() }; + GenericTerm::Triple(Box::new(spo.map(Self::from_term))) + } + TermKind::Variable => { + // the following is safe because we checked term.kind() + let name = unsafe { term.variable().unwrap_unchecked() }; + GenericTerm::Variable(name.map_unchecked(|txt| T::from(&txt))) + } + } + } +} + +impl> From> for GenericTerm { + fn from(value: IriRef) -> Self { + GenericTerm::Iri(value) + } +} + +impl> From> for GenericTerm { + fn from(value: BnodeId) -> Self { + GenericTerm::BlankNode(value) + } +} + +impl> From<(T, IriRef)> for GenericTerm { + fn from(value: (T, IriRef)) -> Self { + GenericTerm::Literal(GenericLiteral::Typed(value.0, value.1)) + } +} + +impl> From<(T, LanguageTag)> for GenericTerm { + fn from(value: (T, LanguageTag)) -> Self { + GenericTerm::Literal(GenericLiteral::LanguageString(value.0, value.1)) + } +} + +impl> From> for GenericTerm { + fn from(value: VarName) -> Self { + GenericTerm::Variable(value) + } +} + +impl> From; 3]>> for GenericTerm { + fn from(value: Box<[GenericTerm; 3]>) -> Self { + GenericTerm::Triple(value) + } +} + +// + +/// This type is mostly required as one of the variants of [`GenericTerm`]. +/// +/// It can however be used as a specialized [`Term`] implementation. +#[derive(Clone, Debug)] +pub enum GenericLiteral> { + /// An RDF [literal](https://www.w3.org/TR/rdf11-concepts/#section-Graph-Literal) + Typed(T, IriRef), + /// An RDF [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string) + LanguageString(T, LanguageTag), +} + +impl> GenericLiteral { + /// The [lexical form](https://www.w3.org/TR/rdf11-concepts/#dfn-lexical-form) of this literal + pub fn get_lexical_form(&self) -> &str { + match self { + GenericLiteral::Typed(lex, ..) => lex, + GenericLiteral::LanguageString(lex, ..) => lex, + } + .borrow() + } + + /// The [datatype](https://www.w3.org/TR/rdf11-concepts/#dfn-datatype-iri) of this literal + pub fn get_datatype(&self) -> IriRef<&str> { + match self { + GenericLiteral::Typed(_, dt) => dt.as_ref(), + GenericLiteral::LanguageString(..) => RDF_LANG_STRING.as_ref(), + } + } + + /// The [language tag](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tag) of this literal, if any + pub fn get_language_tag(&self) -> Option> { + match self { + GenericLiteral::Typed(..) => None, + GenericLiteral::LanguageString(_, tag) => Some(tag.as_ref()), + } + } +} + +impl + Debug> Term for GenericLiteral { + type BorrowTerm<'x> = &'x Self where Self: 'x; + + fn kind(&self) -> TermKind { + TermKind::Literal + } + + fn borrow_term(&self) -> Self::BorrowTerm<'_> { + self + } + + fn lexical_form(&self) -> Option { + Some(MownStr::from_str(self.get_lexical_form())) + } + + fn datatype(&self) -> Option> { + Some(self.get_datatype().map_unchecked(MownStr::from_str)) + } + + fn language_tag(&self) -> Option> { + self.get_language_tag() + .map(|tag| tag.map_unchecked(MownStr::from_str)) + } +} + +impl + for<'x> From<&'x str>> TryFromTerm for GenericLiteral { + type Error = GenericLiteralError; + + fn try_from_term(term: U) -> Result { + if term.is_literal() { + // the following is safe because we checked term.kind() + let lex = unsafe { term.lexical_form().unwrap_unchecked() }; + let lex = T::from(&lex); + if let Some(tag) = term.language_tag() { + Ok(GenericLiteral::LanguageString( + lex, + tag.map_unchecked(|txt| T::from(&txt)), + )) + } else { + // the following is safe because we checked term.kind() + let dt = unsafe { term.datatype().unwrap_unchecked() }; + Ok(GenericLiteral::Typed( + lex, + dt.map_unchecked(|txt| T::from(&txt)), + )) + } + } else { + Err(GenericLiteralError(term.kind())) + } + } +} + +// + +/// Error raised when trying to convert another kind of term to [`GenericLiteral`] +#[derive(Debug)] +pub struct GenericLiteralError(TermKind); + +impl std::fmt::Display for GenericLiteralError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Only literals can be convert to GenericLiteral, found {:?}", + self.0 + ) + } +} + +impl std::error::Error for GenericLiteralError {} + +#[cfg(test)] +mod test { + use super::*; + use sophia_api::{ + ns::xsd, + term::{assert_consistent_term_impl, SimpleTerm}, + }; + + #[test] + fn generic_literal_typed() { + let glit: GenericLiteral = 42.try_into_term().unwrap(); + assert_consistent_term_impl(&glit); + assert_eq!(glit.kind(), TermKind::Literal); + assert_eq!(glit.lexical_form().unwrap(), "42"); + assert!(Term::eq(&glit.datatype().unwrap(), xsd::integer)); + } + + #[test] + fn generic_literal_language_string() { + let en = LanguageTag::new_unchecked("en"); + let glit: GenericLiteral = ("hello" * en).try_into_term().unwrap(); + assert_consistent_term_impl(&glit); + assert_eq!(glit.kind(), TermKind::Literal); + assert_eq!(glit.lexical_form().unwrap(), "hello"); + assert!(Term::eq(&glit.datatype().unwrap(), rdf::langString)); + assert_eq!(glit.language_tag().unwrap(), en); + } + + #[test] + fn generic_literal_from_iri_errs() { + assert!(GenericLiteral::::try_from_term(rdf::type_).is_err()) + } + + #[test] + fn generic_term_iri() { + let gt: GenericTerm = rdf::type_.into_term(); + assert_consistent_term_impl(>); + assert_eq!(gt.kind(), TermKind::Iri); + assert!(Term::eq(>.iri().unwrap(), rdf::type_)); + } + + #[test] + fn generic_term_bnode() { + let bn = BnodeId::new_unchecked("x"); + let gt: GenericTerm = bn.into_term(); + assert_consistent_term_impl(>); + assert_eq!(gt.kind(), TermKind::BlankNode); + assert_eq!(>.bnode_id().unwrap(), "x"); + } + + #[test] + fn generic_term_typed_literal() { + let gt: GenericTerm = 42.into_term(); + assert_consistent_term_impl(>); + assert_eq!(gt.kind(), TermKind::Literal); + assert_eq!(gt.lexical_form().unwrap(), "42"); + assert!(Term::eq(>.datatype().unwrap(), xsd::integer)); + } + + #[test] + fn generic_term_language_string() { + let en = LanguageTag::new_unchecked("en"); + let gt: GenericTerm = ("hello" * en).into_term(); + assert_consistent_term_impl(>); + assert_eq!(gt.kind(), TermKind::Literal); + assert_eq!(gt.lexical_form().unwrap(), "hello"); + assert!(Term::eq(>.datatype().unwrap(), rdf::langString)); + assert_eq!(gt.language_tag().unwrap(), en); + } + + #[test] + fn generic_term_triple() { + let spo = [rdf::type_, rdf::type_, rdf::Property].map(Term::into_term); + let qt = SimpleTerm::Triple(Box::new(spo)); + let gt: GenericTerm = qt.into_term(); + assert_consistent_term_impl(>); + assert_eq!(gt.kind(), TermKind::Triple); + let spo = gt.triple().unwrap(); + assert!(Term::eq(spo[0], rdf::type_)); + assert!(Term::eq(spo[1], rdf::type_)); + assert!(Term::eq(spo[2], rdf::Property)); + } + + #[test] + fn generic_term_variable() { + let v = VarName::new_unchecked("x"); + let gt: GenericTerm = v.into_term(); + assert_consistent_term_impl(>); + assert_eq!(gt.kind(), TermKind::Variable); + assert_eq!(>.variable().unwrap(), "x"); + } +} diff --git a/term/src/_stash.rs b/term/src/_stash.rs new file mode 100644 index 00000000..121daf5d --- /dev/null +++ b/term/src/_stash.rs @@ -0,0 +1,116 @@ +//! A [stash](GenericStash) is a collection of strings that can be reused +//! to avoid allocating identical string multiple times. + +use std::{borrow::Borrow, collections::BTreeSet}; + +use sophia_api::term::{BnodeId, IriRef, LanguageTag, SimpleTerm, Term, VarName}; + +use crate::{GenericLiteral, GenericTerm}; + +/// A [stash](GenericStash) is a collection of strings that can be reused +/// to avoid allocating identical string multiple times. +#[derive(Clone, Debug)] +pub struct GenericStash { + store: BTreeSet, +} + +impl GenericStash +where + T: Ord, +{ + /// Create a new empty stash + pub fn new() -> Self { + Default::default() + } + + /// Retrieve a value from the stash, if present + pub fn get(&self, probe: &Q) -> Option<&T> + where + T: Borrow, + Q: Ord + ?Sized, + { + self.store.get(probe) + } + + /// Retrieve a value from the stash, inserting it if not present + pub fn get_or_insert(&mut self, probe: &Q) -> &T + where + T: Borrow + for<'x> From<&'x Q>, + Q: Ord + ?Sized, + { + if !self.store.contains(probe) { + let ins = T::from(probe); + self.store.insert(ins); + } + let ret = self.store.get(probe); + debug_assert!(ret.is_some()); + // this is safe because we just inserted this element + unsafe { ret.unwrap_unchecked() } + } + + /// How many values are stored in this stash + pub fn len(&self) -> usize { + self.store.len() + } + + /// Is this stash empty? + pub fn is_empty(&self) -> bool { + self.store.is_empty() + } +} + +impl Default for GenericStash { + fn default() -> Self { + Self { + store: BTreeSet::default(), + } + } +} + +impl GenericStash +where + T: Ord + for<'x> From<&'x str> + Borrow + Clone, +{ + /// Copy any [`Borrow`] into an `T` backed on this stash. + pub fn copy_str>(&mut self, iri: U) -> T { + self.get_or_insert(iri.borrow()).clone() + } + + /// Copy any [`IriRef`] into an [`IriRef`] backed on this stash. + pub fn copy_iri>(&mut self, iri: IriRef) -> IriRef { + IriRef::new_unchecked(self.copy_str(iri)) + } + + /// Copy any [`BnodeId`] into an [`BnodeId`] backed on this stash. + pub fn copy_bnode_id>(&mut self, bnid: BnodeId) -> BnodeId { + BnodeId::new_unchecked(self.copy_str(bnid)) + } + + /// Copy any [`LanguageTag`] into an [`LanguageTag`] backed on this stash. + pub fn copy_language_tag>(&mut self, tag: LanguageTag) -> LanguageTag { + LanguageTag::new_unchecked(self.copy_str(tag)) + } + + /// Copy any [`VarName`] into an [`VarName`] backed on this stash. + pub fn copy_var_name>(&mut self, vn: VarName) -> VarName { + VarName::new_unchecked(self.copy_str(vn)) + } + + /// Copy any [`Term`] into an [`GenericTerm`] backed on this stash. + pub fn copy_term(&mut self, t: U) -> GenericTerm { + use SimpleTerm::*; + match t.as_simple() { + Iri(iri) => GenericTerm::Iri(self.copy_iri(iri)), + BlankNode(bnid) => GenericTerm::BlankNode(self.copy_bnode_id(bnid)), + LiteralDatatype(lex, dt) => { + GenericTerm::Literal(GenericLiteral::Typed(self.copy_str(lex), self.copy_iri(dt))) + } + LiteralLanguage(lex, tag) => GenericTerm::Literal(GenericLiteral::LanguageString( + self.copy_str(lex), + self.copy_language_tag(tag), + )), + Triple(tr) => GenericTerm::Triple(Box::new(tr.map(|t| self.copy_term(t)))), + Variable(vn) => GenericTerm::Variable(self.copy_var_name(vn)), + } + } +} diff --git a/term/src/lib.rs b/term/src/lib.rs new file mode 100644 index 00000000..192b06de --- /dev/null +++ b/term/src/lib.rs @@ -0,0 +1,94 @@ +//! I define implementations of [`sophia_api::term::Term`] +//! as well as associated types. +#![deny(missing_docs)] +use std::{rc::Rc, sync::Arc}; + +mod _generic; +pub use _generic::*; +mod _stash; +pub use _stash::*; + +/// A [`Term`](sophia_api::term::Term) implementation +/// using [`Arc`] as the underlying text, +/// making it cheap to clone and thread-safe. +/// +/// See also [`ArcStrStash`]. +pub type ArcTerm = GenericTerm>; + +/// A [`Term`](sophia_api::term::Term) implementation +/// using [`Rc`] as the underlying text, +/// making it cheap to clone. +/// +/// See also [`RcStrStash`]. +pub type RcTerm = GenericTerm>; + +/// A stash for generating [`ArcTerm`]s (with [`copy_term`](GenericStash::copy_term)) +/// or any [`Arc`]. +pub type ArcStrStash = GenericStash>; + +/// A stash for generating [`RcTerm`]s (with [`copy_term`](GenericStash::copy_term)) +/// or any [`Rc`]. +pub type RcStrStash = GenericStash>; + +#[cfg(test)] +mod test { + use sophia_api::term::{BnodeId, FromTerm, SimpleTerm, Term, VarName}; + + use super::*; + + #[test] + #[allow(clippy::needless_borrow,unused_assignments)] + fn arc_str_stash_iri() { + let mut stash = ArcStrStash::new(); + assert_eq!(0, stash.len()); + let mut old_len = stash.len(); + + let t1a = sophia_api::ns::xsd::integer; + let t1b = stash.copy_term(&t1a); + assert!(Term::eq(&t1a, &t1b)); + assert_eq!(old_len + 1, stash.len()); + old_len = stash.len(); + + let t2a = 42; + let t2b = stash.copy_term(t2a); + assert!(Term::eq(&t2a, &t2b)); + assert_eq!(old_len + 1, stash.len()); // datatype was already there + old_len = stash.len(); + + let t3a = "foo"; + let t3b = stash.copy_term(t3a); + assert!(Term::eq(&t3a, &t3b)); + assert_eq!(old_len + 2, stash.len()); // lex + datatype where added + old_len = stash.len(); + + let t4a = "42"; + let t4b = stash.copy_term(t4a); + assert!(Term::eq(&t4a, &t4b)); + assert_eq!(old_len, stash.len()); // all values where alreadt there + old_len = stash.len(); + + let t5a = BnodeId::new_unchecked("foo"); + let t5b = stash.copy_term(&t5a); + assert!(Term::eq(&t5a, &t5b)); + assert_eq!(old_len, stash.len()); // all values where alreadt there + old_len = stash.len(); + + let t6a = VarName::new_unchecked("foobar"); + let t6b = stash.copy_term(&t6a); + assert!(Term::eq(&t6a, &t6b)); + assert_eq!(old_len + 1, stash.len()); // all values where alreadt there + old_len = stash.len(); + + let t1c = stash.copy_term(&t1b); + assert!(Term::eq(&t1a, &t1c)); + assert_eq!(old_len, stash.len()); // all values where alreadt there + old_len = stash.len(); + + let t7a: SimpleTerm<'static> = + SimpleTerm::Triple(Box::new(["s", "p", "o"].map(SimpleTerm::from_term))); + let t7b = stash.copy_term(&t7a); + assert!(Term::eq(&t7a, &t7b)); + assert_eq!(old_len + 3, stash.len()); // all values where alreadt there + old_len = stash.len(); + } +}