1use std::{error::Error, fmt};
20
21use omena_syntax::SymbolKind;
22use smol_str::SmolStr;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub enum NameKind {
26 ClassName,
27 CssIdent,
28 PropertyName,
29 SelectorKey,
30 CustomPropertyName,
31 KeyframesName,
32 MixinName,
33 FilePath,
34}
35
36impl NameKind {
37 pub const ALL: &'static [Self] = &[
38 Self::ClassName,
39 Self::CssIdent,
40 Self::PropertyName,
41 Self::SelectorKey,
42 Self::CustomPropertyName,
43 Self::KeyframesName,
44 Self::MixinName,
45 Self::FilePath,
46 ];
47
48 pub const fn canonical_symbol_kind(self) -> Option<SymbolKind> {
49 match self {
50 Self::ClassName => Some(SymbolKind::Class),
51 Self::CustomPropertyName => Some(SymbolKind::CustomProperty),
52 Self::KeyframesName => Some(SymbolKind::Keyframes),
53 Self::MixinName => Some(SymbolKind::Mixin),
54 Self::CssIdent | Self::PropertyName | Self::SelectorKey | Self::FilePath => None,
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum InternError {
61 EmptyText { kind: NameKind },
62}
63
64impl fmt::Display for InternError {
65 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 Self::EmptyText { kind } => {
68 write!(formatter, "cannot intern empty text for {kind:?}")
69 }
70 }
71 }
72}
73
74impl Error for InternError {}
75
76#[salsa::interned(debug)]
77pub struct ClassName<'db> {
78 #[returns(ref)]
79 pub text: SmolStr,
80}
81
82#[salsa::interned(debug)]
83pub struct CssIdent<'db> {
84 #[returns(ref)]
85 pub text: SmolStr,
86}
87
88#[salsa::interned(debug)]
89pub struct PropertyName<'db> {
90 #[returns(ref)]
91 pub text: SmolStr,
92}
93
94#[salsa::interned(debug)]
95pub struct SelectorKey<'db> {
96 #[returns(ref)]
97 pub text: SmolStr,
98}
99
100#[salsa::interned(debug)]
101pub struct CustomPropertyName<'db> {
102 #[returns(ref)]
103 pub text: SmolStr,
104}
105
106#[salsa::interned(debug)]
107pub struct KeyframesName<'db> {
108 #[returns(ref)]
109 pub text: SmolStr,
110}
111
112#[salsa::interned(debug)]
113pub struct MixinName<'db> {
114 #[returns(ref)]
115 pub text: SmolStr,
116}
117
118#[salsa::interned(debug)]
119pub struct FilePath<'db> {
120 #[returns(ref)]
121 pub text: SmolStr,
122}
123
124pub fn intern_class_name<'db>(
125 db: &'db dyn salsa::Database,
126 text: impl Into<SmolStr>,
127) -> Result<ClassName<'db>, InternError> {
128 checked_text(NameKind::ClassName, text).map(|text| ClassName::new(db, text))
129}
130
131pub fn intern_css_ident<'db>(
132 db: &'db dyn salsa::Database,
133 text: impl Into<SmolStr>,
134) -> Result<CssIdent<'db>, InternError> {
135 checked_text(NameKind::CssIdent, text).map(|text| CssIdent::new(db, text))
136}
137
138pub fn intern_property_name<'db>(
139 db: &'db dyn salsa::Database,
140 text: impl Into<SmolStr>,
141) -> Result<PropertyName<'db>, InternError> {
142 checked_text(NameKind::PropertyName, text).map(|text| PropertyName::new(db, text))
143}
144
145pub fn intern_selector_key<'db>(
146 db: &'db dyn salsa::Database,
147 text: impl Into<SmolStr>,
148) -> Result<SelectorKey<'db>, InternError> {
149 checked_text(NameKind::SelectorKey, text).map(|text| SelectorKey::new(db, text))
150}
151
152pub fn intern_custom_property_name<'db>(
153 db: &'db dyn salsa::Database,
154 text: impl Into<SmolStr>,
155) -> Result<CustomPropertyName<'db>, InternError> {
156 checked_text(NameKind::CustomPropertyName, text).map(|text| CustomPropertyName::new(db, text))
157}
158
159pub fn intern_keyframes_name<'db>(
160 db: &'db dyn salsa::Database,
161 text: impl Into<SmolStr>,
162) -> Result<KeyframesName<'db>, InternError> {
163 checked_text(NameKind::KeyframesName, text).map(|text| KeyframesName::new(db, text))
164}
165
166pub fn intern_mixin_name<'db>(
167 db: &'db dyn salsa::Database,
168 text: impl Into<SmolStr>,
169) -> Result<MixinName<'db>, InternError> {
170 checked_text(NameKind::MixinName, text).map(|text| MixinName::new(db, text))
171}
172
173pub fn intern_file_path<'db>(
174 db: &'db dyn salsa::Database,
175 text: impl Into<SmolStr>,
176) -> Result<FilePath<'db>, InternError> {
177 checked_text(NameKind::FilePath, text).map(|text| FilePath::new(db, text))
178}
179
180fn checked_text(kind: NameKind, text: impl Into<SmolStr>) -> Result<SmolStr, InternError> {
181 let text = text.into();
182 if text.is_empty() {
183 Err(InternError::EmptyText { kind })
184 } else {
185 Ok(text)
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct OmenaInternerBoundarySummaryV0 {
191 pub schema_version: &'static str,
192 pub product: &'static str,
193 pub phase: &'static str,
194 pub name_kind_count: usize,
195 pub salsa_interned_type_count: usize,
196 pub validated_helper_count: usize,
197 pub symbol_mapped_name_kind_count: usize,
198 pub ready_surfaces: Vec<&'static str>,
199 pub next_surfaces: Vec<&'static str>,
200}
201
202pub fn summarize_omena_interner_boundary() -> OmenaInternerBoundarySummaryV0 {
203 OmenaInternerBoundarySummaryV0 {
204 schema_version: "0",
205 product: "omena-interner.boundary",
206 phase: "h1-beta-name-identity-substrate",
207 name_kind_count: NameKind::ALL.len(),
208 salsa_interned_type_count: 8,
209 validated_helper_count: 8,
210 symbol_mapped_name_kind_count: NameKind::ALL
211 .iter()
212 .filter(|kind| kind.canonical_symbol_kind().is_some())
213 .count(),
214 ready_surfaces: vec![
215 "typedSalsaInternedNames",
216 "validatedNameHelpers",
217 "syntaxSymbolKindMapping",
218 "workspaceFilePathIdentity",
219 "parserSemanticNameConsumption",
220 "semanticSoaNameTables",
221 ],
222 next_surfaces: Vec::new(),
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn declares_eight_workspace_name_kinds() {
232 assert_eq!(NameKind::ALL.len(), 8);
233 assert_eq!(
234 NameKind::CustomPropertyName.canonical_symbol_kind(),
235 Some(SymbolKind::CustomProperty),
236 );
237 assert_eq!(NameKind::FilePath.canonical_symbol_kind(), None);
238 }
239
240 #[test]
241 fn interns_equal_text_to_equal_typed_ids() {
242 let db = salsa::DatabaseImpl::default();
243
244 let first = intern_class_name(&db, "button");
245 let second = intern_class_name(&db, SmolStr::new("button"));
246
247 assert!(matches!((&first, &second), (Ok(_), Ok(_))));
248 if let (Ok(first), Ok(second)) = (first, second) {
249 assert_eq!(first, second);
250 assert_eq!(first.text(&db).as_str(), "button");
251 }
252 }
253
254 #[test]
255 fn keeps_name_kinds_type_separated() {
256 let db = salsa::DatabaseImpl::default();
257
258 let class_name = intern_class_name(&db, "primary");
259 let property_name = intern_property_name(&db, "primary");
260
261 assert!(matches!((&class_name, &property_name), (Ok(_), Ok(_))));
262 if let (Ok(class_name), Ok(property_name)) = (class_name, property_name) {
263 assert_eq!(class_name.text(&db), property_name.text(&db));
264 }
265 }
266
267 #[test]
268 fn rejects_empty_names_through_validated_helpers() {
269 let db = salsa::DatabaseImpl::default();
270
271 assert_eq!(
272 intern_mixin_name(&db, ""),
273 Err(InternError::EmptyText {
274 kind: NameKind::MixinName,
275 }),
276 );
277 }
278
279 #[test]
280 fn interns_all_declared_name_surfaces() {
281 let db = salsa::DatabaseImpl::default();
282
283 assert!(intern_css_ident(&db, "display").is_ok());
284 assert!(intern_selector_key(&db, ".button").is_ok());
285 assert!(intern_custom_property_name(&db, "--space").is_ok());
286 assert!(intern_keyframes_name(&db, "fade-in").is_ok());
287 assert!(intern_file_path(&db, "/workspace/Button.module.scss").is_ok());
288 }
289
290 #[test]
291 fn summarizes_phase_beta_name_identity_boundary() {
292 let summary = summarize_omena_interner_boundary();
293
294 assert_eq!(summary.product, "omena-interner.boundary");
295 assert_eq!(summary.phase, "h1-beta-name-identity-substrate");
296 assert_eq!(summary.name_kind_count, 8);
297 assert_eq!(summary.salsa_interned_type_count, 8);
298 assert_eq!(summary.validated_helper_count, 8);
299 assert_eq!(summary.symbol_mapped_name_kind_count, 4);
300 assert!(summary.ready_surfaces.contains(&"typedSalsaInternedNames"));
301 assert!(
302 summary
303 .ready_surfaces
304 .contains(&"parserSemanticNameConsumption")
305 );
306 assert!(summary.ready_surfaces.contains(&"semanticSoaNameTables"));
307 assert!(
308 !summary
309 .next_surfaces
310 .contains(&"parserSemanticNameConsumption")
311 );
312 assert!(!summary.next_surfaces.contains(&"semanticSoaNameTables"));
313 }
314}