Skip to main content

wolfram_expr/
symbol.rs

1//! Representation of Wolfram Language symbols.
2//!
3//! This module provides four primary types:
4//!
5//! * [`Symbol`]
6//! * [`SymbolName`]
7//! * [`Context`]
8//! * [`RelativeContext`]
9//!
10//! These types are used for storing a string value that has been validated to conform
11//! to the syntax of Wolfram Language [symbols and contexts][ref/SymbolNamesAndContexts].
12//!
13//! In addition to the previous types, which own their string value, types are provided
14//! that can be used to validate a borrowed `&str` value, without requiring another
15//! allocation:
16//!
17//! * [`SymbolRef`]
18//! * [`SymbolNameRef`]
19//! * [`ContextRef`]
20// * TODO: `RelativeContextRef`
21//!
22//! ## Related Links
23//!
24//! * [Input Syntax: Symbol Names and Contexts][ref/SymbolNamesAndContexts]
25//!
26//! [ref/SymbolNamesAndContexts]: https://reference.wolfram.com/language/tutorial/InputSyntax.html#6562
27
28pub(crate) mod parse;
29
30use std::{
31    fmt::{self, Debug, Display},
32    mem,
33    sync::Arc,
34};
35
36/* Notes
37
38Operations on Symbols
39
40- Format (with conditional context path based on $Context)
41- Test for equality
42- Lookup symbol name in context path while parsing
43- Remove / format Removed["..."]
44
45*/
46
47//==========================================================
48// Types
49//==========================================================
50
51//======================================
52// Owned Data
53//======================================
54
55// TODO: Change these types to be Arc<str>. This has the consequence of increasing the
56//       size of these types from 64-bits to 128 bits, so first take care that they are
57//       not passed through a C FFI anywhere as a pointer-sized type.
58
59/// Wolfram Language symbol.
60///
61/// # PartialOrd sorting order
62///
63/// The comparison behavior of this type is **NOT** guaranteed to match the behavior of
64/// `` System`Order `` for symbols (and does *not* match it at the moment).
65///
66/// This type implements `PartialOrd`/`Ord` primarily for the purposes of allowing
67/// instances of this type to be included in ordered sets (e.g. `BTreeMap`).
68#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69#[repr(C)]
70pub struct Symbol(Arc<String>);
71
72/// The identifier portion of a symbol. This contains no context marks ('`').
73///
74/// In the symbol `` Global`foo ``, the `SymbolName` is `"foo"`.
75#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
76pub struct SymbolName(Arc<String>);
77
78/// Wolfram Language context.
79///
80/// Examples: `` System` ``, `` Global` ``, `` MyPackage`Utils` ``, etc.
81#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
82pub struct Context(Arc<String>);
83
84/// Context begining with a `` ` ``.
85#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
86pub struct RelativeContext(Arc<String>);
87
88// By using `usize` here, we guarantee that we can later change this to be a pointer
89// instead without changing the sizes of a lot of Expr types. This is good for FFI/ABI
90// compatibility if I decide to change the way Symbol works.
91const _: () = assert!(mem::size_of::<Symbol>() == mem::size_of::<usize>());
92const _: () = assert!(mem::align_of::<Symbol>() == mem::align_of::<usize>());
93
94//======================================
95// Borrowed Data
96//======================================
97
98/// Borrowed string containing a valid symbol.
99#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
100pub struct SymbolRef<'s>(&'s str);
101
102/// Borrowing string containing a valid symbol name.
103#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
104pub struct SymbolNameRef<'s>(&'s str);
105
106/// Borrowed string containing a valid context.
107#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
108pub struct ContextRef<'s>(pub(super) &'s str);
109
110//==========================================================
111// Impls -- Owned Types
112//==========================================================
113
114impl From<&Symbol> for Symbol {
115    fn from(sym: &Symbol) -> Self {
116        sym.clone()
117    }
118}
119
120impl Symbol {
121    /// Attempt to parse `input` as an absolute symbol.
122    ///
123    /// An absolute symbol is a symbol with an explicit context path. ``"System`Plus"`` is
124    /// an absolute symbol, ``"Plus"`` is a relative symbol and/or a [`SymbolName`].
125    /// ``"`Plus"`` is also a relative symbol.
126    pub fn try_new(input: &str) -> Option<Self> {
127        let sym_ref = SymbolRef::try_new(input)?;
128
129        Some(sym_ref.to_symbol())
130    }
131
132    /// Construct a symbol from `input` — stored verbatim, **no validation**.
133    ///
134    /// Any string is accepted: a fully-qualified `` "System`Plus" ``, a
135    /// context-less `"Plus"`, or anything else. What goes into a symbol is the
136    /// caller's business — we don't police WL symbol syntax. A name the kernel
137    /// can't make sense of is the caller's problem, not ours.
138    ///
139    /// ```
140    /// # use wolfram_expr::{Expr, Symbol};
141    /// let expr = Expr::normal(Symbol::new("MyPackage`Foo"), vec![]);
142    /// ```
143    ///
144    /// Use [`Symbol::try_new`] if you *want* to validate the name first.
145    pub fn new(input: &str) -> Self {
146        // SAFETY: a `Symbol` is just an `Arc<String>`; an unusual name is not
147        // unsound, merely (perhaps) meaningless to the kernel.
148        unsafe { Symbol::unchecked_new(input.to_owned()) }
149    }
150
151    /// Get a borrowed [`SymbolRef`] from this [`Symbol`].
152    pub fn as_symbol_ref(&self) -> SymbolRef<'_> {
153        let Symbol(arc_string) = self;
154
155        SymbolRef(arc_string.as_str())
156    }
157
158    /// Get the context path part of a symbol as an [`ContextRef`].
159    pub fn context(&self) -> ContextRef<'_> {
160        self.as_symbol_ref().context()
161    }
162
163    /// Get the symbol name part of a symbol as a [`SymbolNameRef`].
164    pub fn symbol_name(&self) -> SymbolNameRef<'_> {
165        self.as_symbol_ref().symbol_name()
166    }
167}
168
169impl SymbolName {
170    /// Attempt to parse `input` as a symbol name.
171    ///
172    /// A symbol name is a symbol without any context marks.
173    pub fn try_new(input: &str) -> Option<SymbolName> {
174        SymbolNameRef::try_new(input)
175            .as_ref()
176            .map(SymbolNameRef::to_symbol_name)
177    }
178
179    /// Get a borrowed [`SymbolNameRef`] from this `SymbolName`.
180    pub fn as_symbol_name_ref(&self) -> SymbolNameRef<'_> {
181        SymbolNameRef(self.as_str())
182    }
183}
184
185impl Context {
186    /// Attempt to parse `input` as a context.
187    pub fn try_new(input: &str) -> Option<Self> {
188        let context_ref = ContextRef::try_new(input)?;
189
190        Some(context_ref.to_context())
191    }
192
193    /// Construct a context from `input`.
194    ///
195    /// # Panics
196    ///
197    /// This function will panic if `input` is not a valid Wolfram Language context.
198    /// `Context::try_new(input)` must succeed.
199    ///
200    /// This method is intended to be used for convenient construction of contexts from
201    /// string literals, where an error is unlikely to occur, e.g.:
202    ///
203    /// ```
204    /// use wolfram_expr::symbol::Context;
205    ///
206    /// let context = Context::new("MyPackage`");
207    /// ```
208    ///
209    /// If not using a string literal as the argument, prefer to use [`Context::try_new`]
210    /// and handle the error condition.
211    #[track_caller]
212    pub fn new(input: &str) -> Self {
213        match Context::try_new(input) {
214            Some(context) => context,
215            None => panic!("string is not parseable as a context: {}", input),
216        }
217    }
218
219    /// The `` Global` `` context.
220    pub fn global() -> Self {
221        Context(Arc::new(String::from("Global`")))
222    }
223
224    /// The `` System` `` context.
225    pub fn system() -> Self {
226        Context(Arc::new(String::from("System`")))
227    }
228
229    /// Construct a new [`Context`] by appending a new context component to this
230    /// context.
231    ///
232    /// ```
233    /// use wolfram_expr::symbol::{Context, SymbolName, SymbolNameRef};
234    ///
235    /// let context = Context::from_symbol_name(&SymbolName::try_new("MyContext").unwrap());
236    /// let private = context.join(SymbolNameRef::try_new("Private").unwrap());
237    ///
238    /// assert!(private.as_str() == "MyContext`Private`");
239    /// ```
240    pub fn join(&self, name: SymbolNameRef) -> Context {
241        let Context(context) = self;
242        Context::try_new(&format!("{}{}`", context, name.as_str()))
243            .expect("Context::join(): invalid Context")
244    }
245
246    /// Return the components of this [`Context`].
247    ///
248    /// ```
249    /// use wolfram_expr::symbol::Context;
250    ///
251    /// let context = Context::new("MyPackage`Sub`Module`");
252    ///
253    /// let components = context.components();
254    ///
255    /// assert!(components.len() == 3);
256    /// assert!(components[0].as_str() == "MyPackage");
257    /// assert!(components[1].as_str() == "Sub");
258    /// assert!(components[2].as_str() == "Module");
259    /// ```
260    pub fn components(&self) -> Vec<SymbolNameRef<'_>> {
261        let Context(string) = self;
262
263        let comps: Vec<SymbolNameRef> = string
264            .split('`')
265            // Remove the last component, which will always be the empty string
266            .filter(|comp| !comp.is_empty())
267            .map(|comp| {
268                SymbolNameRef::try_new(comp)
269                    .expect("Context::components(): invalid context component")
270            })
271            .collect();
272
273        comps
274    }
275
276    /// Get a borrowed [`ContextRef`] from this `Context`.
277    pub fn as_context_ref(&self) -> ContextRef<'_> {
278        ContextRef(self.as_str())
279    }
280
281    /// Create the context `` name` ``.
282    pub fn from_symbol_name(name: &SymbolName) -> Self {
283        Context::try_new(&format!("{}`", name)).unwrap()
284    }
285}
286
287impl RelativeContext {
288    /// Attempt to parse `input` as a relative context.
289    pub fn try_new(input: &str) -> Option<Self> {
290        crate::symbol::parse::RelativeContext_try_new(input)
291    }
292
293    /// Return the components of this [`RelativeContext`].
294    ///
295    /// ```
296    /// use wolfram_expr::symbol::RelativeContext;
297    ///
298    /// let context = RelativeContext::try_new("`Sub`Module`").unwrap();
299    ///
300    /// let components = context.components();
301    ///
302    /// assert!(components.len() == 2);
303    /// assert!(components[0].as_str() == "Sub");
304    /// assert!(components[1].as_str() == "Module");
305    /// ```
306    pub fn components(&self) -> Vec<SymbolNameRef<'_>> {
307        let RelativeContext(string) = self;
308
309        let comps: Vec<SymbolNameRef> = string
310            .split('`')
311            // Remove the last component, which will always be the empty string
312            .filter(|comp| !comp.is_empty())
313            .map(|comp| {
314                SymbolNameRef::try_new(comp)
315                    .expect("RelativeContext::components(): invalid context component")
316            })
317            .collect();
318
319        comps
320    }
321}
322
323macro_rules! common_impls {
324    (impl $ty:ident) => {
325        impl Display for $ty {
326            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
327                let $ty(string) = self;
328
329                write!(f, "{}", string)
330            }
331        }
332
333        impl $ty {
334            /// Get the underlying `&str` representation of this type.
335            pub fn as_str(&self) -> &str {
336                let $ty(string) = self;
337
338                string.as_str()
339            }
340
341            /// Create a new instance of this type from a string, without validating the
342            /// string contents.
343            ///
344            /// It's up to the caller to ensure that the passed `input` has the correct
345            /// syntax.
346            ///
347            /// ## Safety
348            ///
349            /// This function actually does not do anything that would be rejected by
350            /// rustc were the function not marked `unsafe`. However, this function is so
351            /// often *not* what is really needed, it's marked unsafe as a deterent to
352            /// possible users.
353            pub(crate) unsafe fn unchecked_new<S: Into<String>>(input: S) -> $ty {
354                let inner: Arc<String> = Arc::new(input.into());
355                $ty(inner)
356            }
357        }
358    };
359}
360
361common_impls!(impl Symbol);
362common_impls!(impl SymbolName);
363common_impls!(impl Context);
364common_impls!(impl RelativeContext);
365
366//==========================================================
367// Impls -- Borrowed Types
368//==========================================================
369
370impl<'s> SymbolRef<'s> {
371    /// Attempt to parse `string` as an absolute symbol.
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// use wolfram_expr::symbol::SymbolRef;
377    ///
378    /// assert!(matches!(SymbolRef::try_new("System`List"), Some(_)));
379    /// assert!(matches!(SymbolRef::try_new("List"), None));
380    /// assert!(matches!(SymbolRef::try_new("123"), None));
381    /// ```
382    pub fn try_new(string: &'s str) -> Option<Self> {
383        crate::symbol::parse::SymbolRef_try_new(string)
384    }
385
386    /// Get the borrowed string data.
387    pub fn as_str(&self) -> &'s str {
388        let SymbolRef(string) = self;
389        string
390    }
391
392    /// Convert this borrowed string into an owned [`Symbol`].
393    pub fn to_symbol(&self) -> Symbol {
394        let SymbolRef(string) = self;
395        unsafe { Symbol::unchecked_new(string.to_owned()) }
396    }
397
398    // TODO: Document this method
399    #[doc(hidden)]
400    pub const unsafe fn unchecked_new(string: &'s str) -> Self {
401        SymbolRef(string)
402    }
403
404    /// Get the context path part of a symbol as an [`ContextRef`].
405    pub fn context(&self) -> ContextRef<'s> {
406        let string = self.as_str();
407
408        let last_grave = string
409            .rfind('`')
410            .expect("Failed to find grave '`' character in symbol");
411
412        // SAFETY: All valid Symbol's will contain at least one grave mark '`', will
413        //         have at least 1 character after that grave mark, and the string up
414        //         to and including the last grave mark will be a valid absolute context.
415        let (context, _) = string.split_at(last_grave + 1);
416
417        unsafe { ContextRef::unchecked_new(context) }
418    }
419
420    /// Get the symbol name part of a symbol as a [`SymbolNameRef`].
421    pub fn symbol_name(&self) -> SymbolNameRef<'s> {
422        let string = self.as_str();
423
424        let last_grave = string
425            .rfind('`')
426            .expect("Failed to find grave '`' character in symbol");
427
428        // SAFETY: All valid Symbol's will contain at least one grave mark '`', will
429        //         have at least 1 character after that grave mark, and the string up
430        //         to and including the last grave mark will be a valid absolute context.
431        let (_, name) = string.split_at(last_grave + 1);
432        unsafe { SymbolNameRef::unchecked_new(name) }
433    }
434}
435
436impl<'s> SymbolNameRef<'s> {
437    /// Attempt to parse `string` as a symbol name.
438    pub fn try_new(string: &'s str) -> Option<Self> {
439        crate::symbol::parse::SymbolNameRef_try_new(string)
440    }
441
442    /// Get the borrowed string data.
443    pub fn as_str(&self) -> &'s str {
444        let SymbolNameRef(string) = self;
445        string
446    }
447
448    /// Convert this borrowed string into an owned [`SymbolName`].
449    pub fn to_symbol_name(&self) -> SymbolName {
450        let SymbolNameRef(string) = self;
451        unsafe { SymbolName::unchecked_new(string.to_owned()) }
452    }
453
454    #[doc(hidden)]
455    pub unsafe fn unchecked_new(string: &'s str) -> Self {
456        SymbolNameRef(string)
457    }
458}
459
460impl<'s> ContextRef<'s> {
461    /// Attempt to parse `string` as a context.
462    pub fn try_new(string: &'s str) -> Option<Self> {
463        crate::symbol::parse::ContextRef_try_new(string)
464    }
465
466    /// Get the borrowed string data.
467    pub fn as_str(&self) -> &'s str {
468        let ContextRef(string) = self;
469        string
470    }
471
472    /// Convert this borrowed string into an owned [`Context`].
473    pub fn to_context(&self) -> Context {
474        let ContextRef(string) = self;
475        unsafe { Context::unchecked_new(string.to_owned()) }
476    }
477
478    #[doc(hidden)]
479    pub unsafe fn unchecked_new(string: &'s str) -> Self {
480        ContextRef(string)
481    }
482}
483
484//======================================
485// Formatting impls
486//======================================
487
488impl Display for SymbolNameRef<'_> {
489    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
490        write!(f, "{}", self.as_str())
491    }
492}