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