microcad_lang/syntax/identifier/
mod.rs

1// Copyright © 2024-2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! µcad identifier syntax elements
5
6mod identifier_list;
7mod qualified_name;
8
9use derive_more::{Deref, DerefMut};
10pub use identifier_list::*;
11pub use qualified_name::*;
12
13use crate::{Id, parse::*, parser::Parser, src_ref::*, syntax::*};
14
15/// µcad identifier
16#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
17pub struct Identifier(pub Refer<Id>);
18
19#[derive(Deref, DerefMut, Default)]
20pub(crate) struct IdentifierSet(indexmap::IndexSet<Identifier>);
21
22impl std::iter::FromIterator<Identifier> for IdentifierSet {
23    fn from_iter<T: IntoIterator<Item = Identifier>>(iter: T) -> Self {
24        IdentifierSet(indexmap::IndexSet::from_iter(iter))
25    }
26}
27
28static UNIQUE_ID_NEXT: std::sync::Mutex<usize> = std::sync::Mutex::new(0);
29
30/// A case for an identifier.
31#[derive(Debug, PartialEq, Eq)]
32pub enum Case {
33    /// PascalCase
34    Pascal,
35    /// lower_snake_case
36    LowerSnake,
37    /// UPPER_SNAKE_CASE
38    UpperSnake,
39    /// A
40    UpperSingleChar,
41    /// Invalid.
42    Invalid,
43}
44
45impl Identifier {
46    /// Make empty (invalid) id
47    pub fn none() -> Self {
48        Self(Refer::none("".into()))
49    }
50
51    /// Create new identifier with a new unique name.
52    ///
53    /// Every call will return a new identifier (which is a `$` followed by an counter)
54    pub fn unique() -> Self {
55        let mut num = UNIQUE_ID_NEXT
56            .lock()
57            .expect("lock on UNIQUE_ID_NEXT failed");
58        let id = format!("${num}");
59        *num += 1;
60        Identifier::no_ref(&id)
61    }
62
63    /// Check if id is the `super` id
64    pub fn is_super(&self) -> bool {
65        *self.0 == "super"
66    }
67
68    /// Check if this was created with none()
69    pub fn is_none(&self) -> bool {
70        self.0.src_ref().is_empty() && self.0.is_empty()
71    }
72
73    /// Make empty (invalid) id
74    pub fn no_ref(id: &str) -> Self {
75        Self(Refer::none(id.into()))
76    }
77
78    /// Get the value of the identifier
79    pub fn id(&self) -> &Id {
80        &self.0.value
81    }
82
83    /// Return number of identifiers in name
84    pub fn len(&self) -> usize {
85        self.0.len()
86    }
87
88    /// Return if name is empty
89    pub fn is_empty(&self) -> bool {
90        self.0.is_empty()
91    }
92
93    /// check if this is a valid identifier (contains only `A`-`Z`, `a`-`z` or `_`)
94    pub fn validate(self) -> ParseResult<Self> {
95        Parser::parse_rule(crate::parser::Rule::identifier, self.id().as_str(), 0)
96    }
97
98    /// Add given `prefix` to identifier to get `qualified name`.
99    pub fn with_prefix(&self, prefix: &QualifiedName) -> QualifiedName {
100        QualifiedName::from(self).with_prefix(prefix)
101    }
102
103    /// Detect if the identifier matches a certain case.
104    pub fn detect_case(&self) -> Case {
105        let s = &self.0.value;
106
107        if s.is_empty() {
108            return Case::Invalid;
109        }
110
111        if s.len() == 1 {
112            let c = s.chars().next().expect("At least one char");
113            if c.is_ascii_uppercase() {
114                return Case::UpperSingleChar;
115            } else {
116                return Case::Invalid;
117            }
118        }
119
120        let has_underscore = s.contains('_');
121
122        if has_underscore {
123            if s.chars().all(|c| c.is_ascii_uppercase() || c == '_') {
124                return Case::UpperSnake;
125            } else if s.chars().all(|c| c.is_ascii_lowercase() || c == '_') {
126                return Case::LowerSnake;
127            } else {
128                return Case::Invalid;
129            }
130        } else {
131            // Must be PascalCase: starts with uppercase and contains no underscores
132            let mut chars = s.chars();
133            if let Some(first) = chars.next() {
134                if first.is_ascii_uppercase() && chars.all(|c| c.is_ascii_alphanumeric()) {
135                    return Case::Pascal;
136                }
137            }
138        }
139
140        Case::Invalid
141    }
142}
143
144impl SrcReferrer for Identifier {
145    fn src_ref(&self) -> SrcRef {
146        self.0.src_ref.clone()
147    }
148}
149
150impl std::hash::Hash for Identifier {
151    fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
152        self.0.hash(hasher)
153    }
154}
155
156impl std::str::FromStr for Identifier {
157    type Err = crate::eval::EvalError;
158
159    fn from_str(id: &str) -> Result<Self, Self::Err> {
160        Ok(Identifier::no_ref(id).validate()?)
161    }
162}
163
164impl From<&std::ffi::OsStr> for Identifier {
165    fn from(value: &std::ffi::OsStr) -> Self {
166        Identifier::no_ref(value.to_string_lossy().to_string().as_str())
167    }
168}
169
170#[cfg(test)]
171impl From<&str> for Identifier {
172    fn from(value: &str) -> Self {
173        Self(Refer::none(value.into()))
174    }
175}
176
177#[cfg(not(test))]
178impl TryFrom<&str> for Identifier {
179    type Error = ParseError;
180
181    fn try_from(value: &str) -> Result<Self, Self::Error> {
182        Parser::parse_rule(crate::parser::Rule::identifier, value, 0)
183    }
184}
185
186impl<'a> From<&'a Identifier> for &'a str {
187    fn from(value: &'a Identifier) -> Self {
188        &value.0
189    }
190}
191
192impl std::fmt::Display for Identifier {
193    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
194        if self.is_empty() {
195            write!(f, crate::invalid_no_ansi!(ID))
196        } else {
197            write!(f, "{}", self.0)
198        }
199    }
200}
201
202impl std::fmt::Debug for Identifier {
203    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
204        if self.is_empty() {
205            write!(f, "{}", crate::invalid!(ID))
206        } else {
207            write!(f, "{}", self.0)
208        }
209    }
210}
211
212impl PartialEq<str> for Identifier {
213    fn eq(&self, other: &str) -> bool {
214        *self.0 == other
215    }
216}
217
218impl TreeDisplay for Identifier {
219    fn tree_print(&self, f: &mut std::fmt::Formatter, depth: TreeState) -> std::fmt::Result {
220        writeln!(f, "{:depth$}Identifier: {}", "", self.id())
221    }
222}
223
224impl std::fmt::Display for IdentifierSet {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        write!(
227            f,
228            "{}",
229            self.iter()
230                .map(|id| id.to_string())
231                .collect::<Vec<_>>()
232                .join(", ")
233        )
234    }
235}
236
237impl std::fmt::Debug for IdentifierSet {
238    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        write!(
240            f,
241            "{}",
242            self.iter()
243                .map(|id| format!("{id:?}"))
244                .collect::<Vec<_>>()
245                .join(", ")
246        )
247    }
248}
249
250#[test]
251fn identifier_comparison() {
252    use crate::syntax::*;
253
254    // same id but different src refs
255    let id1 = Identifier::no_ref("x");
256    let id2 = Identifier(Refer::new("x".into(), SrcRef::new(0..5, 0, 1, 1)));
257
258    // shall be equal
259    assert!(id1 == id2);
260}
261
262#[test]
263fn identifier_hash() {
264    use crate::syntax::*;
265    use std::hash::{Hash, Hasher};
266
267    // same id but different src refs
268    let id1 = Identifier(Refer::none("x".into()));
269    let id2 = Identifier(Refer::new("x".into(), SrcRef::new(0..5, 0, 1, 1)));
270
271    let mut hasher = std::hash::DefaultHasher::new();
272    id1.hash(&mut hasher);
273    let hash1 = hasher.finish();
274    let mut hasher = std::hash::DefaultHasher::new();
275    id2.hash(&mut hasher);
276
277    let hash2 = hasher.finish();
278
279    // shall be equal
280    assert_eq!(hash1, hash2);
281}
282
283#[test]
284fn identifier_case() {
285    let detect_case = |s| -> Case { Identifier::no_ref(s).detect_case() };
286
287    assert_eq!(detect_case("PascalCase"), Case::Pascal);
288    assert_eq!(detect_case("lower_snake_case"), Case::LowerSnake);
289    assert_eq!(detect_case("UPPER_SNAKE_CASE"), Case::UpperSnake);
290    assert_eq!(detect_case("notValid123_"), Case::Invalid);
291    assert_eq!(detect_case(""), Case::Invalid);
292    assert_eq!(detect_case("A"), Case::UpperSingleChar); // New case
293    assert_eq!(detect_case("z"), Case::Invalid); // lowercase single letter
294    assert_eq!(detect_case("_"), Case::Invalid); // only underscore
295    assert_eq!(detect_case("a_b"), Case::LowerSnake);
296    assert_eq!(detect_case("A_B"), Case::UpperSnake);
297
298    println!("All tests passed.");
299}