Skip to main content

microcad_lang_base/
identifier.rs

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