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