caixa_theme/style.rs
1//! The small semantic-style enum every caixa tool agrees on.
2
3use serde::{Deserialize, Serialize};
4
5#[derive(
6 Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, gen_platform::IsVariant,
7)]
8pub enum Semantic {
9 /// Language keywords — `defcaixa`, `defteia`, `let`, `lambda`, etc.
10 Keyword,
11 /// Non-keyword symbols — identifiers, function names, variant names.
12 Symbol,
13 /// `:keyword-positioned` atoms.
14 KeywordArg,
15 /// `"string literals"`.
16 String,
17 /// `42`, `3.14`.
18 Number,
19 /// `#t`, `#f`, `nil`.
20 Literal,
21 /// `; comments`.
22 Comment,
23 /// Primary accent — useful for highlights, carets, focused tokens.
24 Accent,
25 /// Dim text — metadata, line numbers, help text.
26 Muted,
27
28 // Diagnostic severities.
29 Error,
30 Warning,
31 Info,
32 Hint,
33
34 // Diff decorations — used by formatter preview and lint output.
35 Added,
36 Removed,
37 Unchanged,
38}
39
40impl Semantic {
41 /// Every variant of [`Semantic`] in declaration order.
42 ///
43 /// The single canonical arm-list every substrate consumer that has
44 /// to walk the closed 15-arm semantic-style partition (the two
45 /// theme overlays' exhaustive-match resolver functions
46 /// `blackmatter_dark_color` / `blackmatter_light_color` in
47 /// [`crate::blackmatter`], the future LSP-side per-Semantic
48 /// `SemanticTokenType` dispatch at
49 /// `caixa-lsp/src/main.rs`, a future
50 /// `feira lint --list-styles` operator-facing enumeration verb)
51 /// reads for. Peer of the sibling closed-set fieldless typed
52 /// enums' `ALL` slices already carried by
53 /// [`caixa_core::CaixaKind`] /
54 /// [`caixa_core::supervisor::RestartStrategy`] /
55 /// [`caixa_core::supervisor::RestartPolicy`] /
56 /// [`caixa_core::aplicacao::PlacementStrategy`] /
57 /// [`caixa_core::upgrade::UpgradeInstruction`] /
58 /// `caixa_lint::diagnostic::Severity` /
59 /// `caixa_lint::diagnostic::FixSafety` /
60 /// `caixa_arch::InvariantKind` / `caixa_arch::ArchVerdict` /
61 /// `caixa_provedor::FerriteRuntime` closed-set typed-enum
62 /// discriminator axes.
63 pub const ALL: &'static [Self] = &[
64 Self::Keyword,
65 Self::Symbol,
66 Self::KeywordArg,
67 Self::String,
68 Self::Number,
69 Self::Literal,
70 Self::Comment,
71 Self::Accent,
72 Self::Muted,
73 Self::Error,
74 Self::Warning,
75 Self::Info,
76 Self::Hint,
77 Self::Added,
78 Self::Removed,
79 Self::Unchanged,
80 ];
81
82 /// Canonical kebab-case discriminator scalar for this variant — the
83 /// single substrate-primitive `&'static str` projection every
84 /// downstream consumer of the closed 16-arm [`Semantic`] partition
85 /// (a future LSP-side per-Semantic `SemanticTokenType` name-mapping
86 /// dispatch at `caixa-lsp/src/main.rs`, a future
87 /// `feira lint --list-styles` operator-facing enumeration verb, a
88 /// future `caixa.nvim` per-Semantic highlight-group name resolver
89 /// that reaches for a stable kebab identifier per arm, a future
90 /// `blackmatter-shell` per-arm classname the terminal emitter
91 /// composes into a `data-semantic="<kebab>"` attribute) reaches
92 /// through. Kebab-case matches the peer
93 /// [`gen_platform::IsVariant`]-derived kebab discriminant convention
94 /// the sibling closed-set typed enums
95 /// ([`caixa_core::CaixaKind::as_str`],
96 /// [`caixa_core::supervisor::RestartStrategy::as_str`],
97 /// [`caixa_core::supervisor::RestartPolicy::as_str`],
98 /// [`caixa_core::aplicacao::PlacementStrategy::as_str`],
99 /// [`caixa_core::upgrade::UpgradeInstruction::as_str`],
100 /// `caixa_lint::diagnostic::Severity::as_str`,
101 /// `caixa_lint::diagnostic::FixSafety::as_str`,
102 /// `caixa_arch::InvariantKind::as_str`,
103 /// `caixa_arch::ArchVerdict::as_str`,
104 /// `caixa_provedor::FerriteRuntime::variant_slug`) already emit
105 /// on their canonical `&'static str` projection axis.
106 ///
107 /// The 16 arms return the kebab-case forms of their `PascalCase`
108 /// variant names (`Keyword` → `"keyword"`, `KeywordArg` →
109 /// `"keyword-arg"`, `Unchanged` → `"unchanged"`, etc.), matching
110 /// the peer closed-set typed-enum canonical byte-string conventions.
111 ///
112 /// Peer of the [`std::fmt::Display`] and [`AsRef<str>`] impls on
113 /// this enum, which both route through this accessor so
114 /// `format!("{s}")`, `s.as_str()`, and
115 /// `<Semantic as AsRef<str>>::as_ref(&s)` resolve to the same
116 /// per-arm byte-string.
117 #[must_use]
118 pub const fn as_str(self) -> &'static str {
119 match self {
120 Self::Keyword => "keyword",
121 Self::Symbol => "symbol",
122 Self::KeywordArg => "keyword-arg",
123 Self::String => "string",
124 Self::Number => "number",
125 Self::Literal => "literal",
126 Self::Comment => "comment",
127 Self::Accent => "accent",
128 Self::Muted => "muted",
129 Self::Error => "error",
130 Self::Warning => "warning",
131 Self::Info => "info",
132 Self::Hint => "hint",
133 Self::Added => "added",
134 Self::Removed => "removed",
135 Self::Unchanged => "unchanged",
136 }
137 }
138}
139
140/// [`std::fmt::Display`] routed through [`Semantic::as_str`], so the
141/// pretty-printed byte-string every consumer that formats the semantic
142/// style as user-facing text lands on (a future `feira lint --list-styles`
143/// operator-facing enumeration verb's per-arm line, a future
144/// `caixa-lsp` diagnostic-source line naming the offending semantic,
145/// a future `caixa.nvim` per-Semantic highlight-group name emitter,
146/// a `tracing::field::display(&sem)` structured-log recorder on the
147/// paint-side emit path) reaches for the same lifted kebab-case
148/// per-arm byte-string [`Semantic::as_str`] returns.
149///
150/// Peer of the sibling [`std::fmt::Display`] impls on the closed-set
151/// typed enums the substrate carries — [`caixa_core::CaixaKind`],
152/// [`caixa_core::supervisor::RestartStrategy`],
153/// [`caixa_core::supervisor::RestartPolicy`],
154/// [`caixa_core::aplicacao::PlacementStrategy`],
155/// [`caixa_core::upgrade::UpgradeInstruction`],
156/// `caixa_lint::diagnostic::Severity`,
157/// `caixa_lint::diagnostic::FixSafety`,
158/// `caixa_arch::InvariantKind`, `caixa_arch::ArchVerdict`,
159/// `caixa_provedor::FerriteRuntime` — extended to the second-to-last
160/// un-lifted `caixa-theme` closed-set fieldless typed enum on the
161/// substrate-wide `(as_str, AsRef<str>, Display)` canonical-projection
162/// triple ratchet the prior lifts converged onto.
163impl std::fmt::Display for Semantic {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 f.write_str(self.as_str())
166 }
167}
168
169/// Substrate-canonical [`AsRef<str>`] projection on the caixa-theme
170/// [`Semantic`] closed-set fieldless typed enum — routes through the
171/// same [`Semantic::as_str`] `pub const fn` scalar accessor the paired
172/// [`std::fmt::Display`] impl already reaches for.
173///
174/// Peer of the sibling [`AsRef<str>`] impls the substrate carries on
175/// [`caixa_core::CaixaKind`], [`caixa_core::CaixaVersion`],
176/// [`caixa_core::CaixaDialeto`], [`caixa_core::dep::DepList`],
177/// [`caixa_core::supervisor::RestartStrategy`],
178/// [`caixa_core::supervisor::RestartPolicy`],
179/// [`caixa_core::aplicacao::PlacementStrategy`],
180/// [`caixa_core::aplicacao::RateLimitUnit`],
181/// `caixa_lint::diagnostic::Severity`,
182/// `caixa_arch::InvariantKind`, `caixa_arch::ArchVerdict`,
183/// `caixa_provedor::FerriteRuntime` — extends the substrate-wide
184/// `(as_str, AsRef<str>, Display)` canonical-projection triple onto
185/// the caixa-theme closed-set typed-enum axis, so a future consumer
186/// bound through the trait-idiomatic `.as_ref()` (a
187/// `HashMap::get::<str>(sem.as_ref())` per-Semantic style-lookup, a
188/// future `caixa-lsp` `SemanticTokenType::new(sem.as_ref())`
189/// registration site, any `impl AsRef<str>`-bound generic function)
190/// reaches the same kebab byte-string [`Semantic::as_str`] returns
191/// rather than an open-coded `.as_str()` projection at every
192/// wire-up.
193impl AsRef<str> for Semantic {
194 fn as_ref(&self) -> &str {
195 self.as_str()
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn semantic_all_enumerates_every_variant_in_declaration_order() {
205 // Fail-before-pass-after pin on the [`Semantic::ALL`] slice:
206 // the slice must list every one of the 15 variants in
207 // declaration order (Keyword → Symbol → KeywordArg → String →
208 // Number → Literal → Comment → Accent → Muted → Error →
209 // Warning → Info → Hint → Added → Removed → Unchanged). Peer
210 // of the sibling ALL slices on the closed-set typed-enum
211 // discriminator axes ([`caixa_core::CaixaKind::ALL`],
212 // [`caixa_core::supervisor::RestartStrategy::ALL`],
213 // [`caixa_core::supervisor::RestartPolicy::ALL`],
214 // [`caixa_core::aplicacao::PlacementStrategy::ALL`],
215 // [`caixa_core::upgrade::UpgradeInstruction::ALL`]). A future
216 // arm addition (a `Namespace` tier between `Symbol` and
217 // `KeywordArg` for the M4 tatara-lisp module system's
218 // qualified-name semantic-token dispatch, a `Deleted` tier
219 // for a hard-delete-mark distinct from `Removed` the future
220 // 3-way diff surface grows) that lands the arm on the enum
221 // but forgets to extend `ALL` must trip this pin rather than
222 // surface as a downstream consumer's silently-partial
223 // iteration.
224 assert_eq!(
225 Semantic::ALL,
226 &[
227 Semantic::Keyword,
228 Semantic::Symbol,
229 Semantic::KeywordArg,
230 Semantic::String,
231 Semantic::Number,
232 Semantic::Literal,
233 Semantic::Comment,
234 Semantic::Accent,
235 Semantic::Muted,
236 Semantic::Error,
237 Semantic::Warning,
238 Semantic::Info,
239 Semantic::Hint,
240 Semantic::Added,
241 Semantic::Removed,
242 Semantic::Unchanged,
243 ],
244 );
245 // Also pin the per-arm `IsVariant`-derived partition: every
246 // arm in `ALL` must satisfy exactly one of the 15 generated
247 // arm-discriminator predicates.
248 for variant in Semantic::ALL {
249 let row = [
250 variant.is_keyword(),
251 variant.is_symbol(),
252 variant.is_keyword_arg(),
253 variant.is_string(),
254 variant.is_number(),
255 variant.is_literal(),
256 variant.is_comment(),
257 variant.is_accent(),
258 variant.is_muted(),
259 variant.is_error(),
260 variant.is_warning(),
261 variant.is_info(),
262 variant.is_hint(),
263 variant.is_added(),
264 variant.is_removed(),
265 variant.is_unchanged(),
266 ];
267 let hits = row.iter().filter(|b| **b).count();
268 assert_eq!(
269 hits, 1,
270 "Semantic::{variant:?} must satisfy exactly one of the \
271 15 is_* arm-discriminator predicates; got {row:?}",
272 );
273 }
274 }
275
276 #[test]
277 fn semantic_is_variant_predicates_partition_the_arm_set() {
278 // Fail-before-pass-after pin on the [`gen_platform::IsVariant`]
279 // derive: for each of the 15 variants, exactly one of the
280 // generated is_* predicates returns `true` and the other 14
281 // return `false`. Pre-derive the closed 15-arm partition
282 // lived only inside the two theme overlays' 15-arm match
283 // resolvers; a future rebrand (a `#[is_variant(name = "…")]`
284 // drift, a manual hand-rolled `impl` that shadows the
285 // derive-generated method, an arm rename) trips this pin at
286 // caixa-theme build time rather than surfacing far from the
287 // derive declaration. Peer of the sibling
288 // [`caixa_core::CaixaKind`] `IsVariant` partition pin.
289 // A copy-paste flip that reroutes one arm through the wrong
290 // predicate lane trips at the identity-diagonal assertion,
291 // since each variant's row is generated live from `ALL`'s
292 // declaration order rather than transcribed by hand.
293 for (idx, variant) in Semantic::ALL.iter().enumerate() {
294 let observed: [bool; 16] = [
295 variant.is_keyword(),
296 variant.is_symbol(),
297 variant.is_keyword_arg(),
298 variant.is_string(),
299 variant.is_number(),
300 variant.is_literal(),
301 variant.is_comment(),
302 variant.is_accent(),
303 variant.is_muted(),
304 variant.is_error(),
305 variant.is_warning(),
306 variant.is_info(),
307 variant.is_hint(),
308 variant.is_added(),
309 variant.is_removed(),
310 variant.is_unchanged(),
311 ];
312 let mut expected = [false; 16];
313 expected[idx] = true;
314 assert_eq!(
315 observed, expected,
316 "Semantic::{variant:?} at ALL[{idx}] is_* predicates \
317 must fire only on their own arm lane (identity \
318 diagonal); got {observed:?}",
319 );
320 }
321 }
322
323 #[test]
324 fn semantic_as_str_returns_canonical_kebab_case_per_arm() {
325 // Fail-before-pass-after per-arm byte-string pin on
326 // [`Semantic::as_str`] — the substrate-canonical `&'static str`
327 // projection every downstream consumer of the closed 16-arm
328 // partition reaches through. A future arm rename (a `Symbol` →
329 // `Identifier` rebrand tracking a hypothetical LSP-side
330 // `SemanticTokenType` reshuffle, an `Accent` → `Highlight`
331 // rebrand tracking a `blackmatter-shell` classname rework) that
332 // touches the enum arm but forgets to update the paired
333 // `as_str` arm — or vice versa — trips this pin at caixa-theme
334 // build time rather than surfacing as a downstream
335 // `feira lint --list-styles` operator-facing enumeration verb's
336 // silently-renamed row far from the two-declaration site.
337 //
338 // Kebab-case matches the peer [`gen_platform::IsVariant`]-
339 // derived kebab discriminant convention the sibling closed-set
340 // typed enums already emit on their canonical byte-string
341 // projection axis.
342 assert_eq!(Semantic::Keyword.as_str(), "keyword");
343 assert_eq!(Semantic::Symbol.as_str(), "symbol");
344 assert_eq!(Semantic::KeywordArg.as_str(), "keyword-arg");
345 assert_eq!(Semantic::String.as_str(), "string");
346 assert_eq!(Semantic::Number.as_str(), "number");
347 assert_eq!(Semantic::Literal.as_str(), "literal");
348 assert_eq!(Semantic::Comment.as_str(), "comment");
349 assert_eq!(Semantic::Accent.as_str(), "accent");
350 assert_eq!(Semantic::Muted.as_str(), "muted");
351 assert_eq!(Semantic::Error.as_str(), "error");
352 assert_eq!(Semantic::Warning.as_str(), "warning");
353 assert_eq!(Semantic::Info.as_str(), "info");
354 assert_eq!(Semantic::Hint.as_str(), "hint");
355 assert_eq!(Semantic::Added.as_str(), "added");
356 assert_eq!(Semantic::Removed.as_str(), "removed");
357 assert_eq!(Semantic::Unchanged.as_str(), "unchanged");
358 }
359
360 #[test]
361 fn semantic_as_str_projections_are_all_distinct_across_arms() {
362 // Fail-before-pass-after pin on the injectivity of
363 // [`Semantic::as_str`]'s projection — no two arms may share
364 // their canonical kebab byte-string, since a future consumer
365 // that keys a per-arm dispatch table off the projection (a
366 // `HashMap::<&str, _>::from_iter(Semantic::ALL.iter().map(|s|
367 // (s.as_str(), …)))` style-lookup, a future `caixa-lsp`
368 // `SemanticTokenType::new(sem.as_ref())` registration table
369 // keyed by kebab identifier, a future `feira lint --list-styles`
370 // one-row-per-arm enumeration table) would silently collapse
371 // the two colliding arms onto one entry, dropping the second
372 // insertion. A future arm addition (a `Namespace` tier between
373 // `Symbol` and `KeywordArg` for the M4 tatara-lisp module
374 // system's qualified-name semantic-token dispatch, a `Deleted`
375 // tier for a hard-delete-mark distinct from `Removed` a future
376 // 3-way diff surface grows) that lands the arm on the enum and
377 // reuses a peer arm's kebab identifier (a copy-paste-derived
378 // `"removed"` on the new `Deleted` arm) trips this pin rather
379 // than surfacing far from the arm addition site.
380 let mut projections: Vec<&'static str> = Semantic::ALL.iter().map(|s| s.as_str()).collect();
381 let before = projections.len();
382 projections.sort_unstable();
383 projections.dedup();
384 assert_eq!(
385 projections.len(),
386 before,
387 "Semantic::as_str must be injective across ALL — collisions: \
388 {projections:?}",
389 );
390 }
391
392 #[test]
393 fn semantic_display_and_as_ref_str_route_through_as_str_accessor() {
394 // Fail-before-pass-after three-path convergence pin on the
395 // substrate-wide `(as_str, AsRef<str>, Display)` canonical-
396 // projection triple for the caixa-theme [`Semantic`] closed-set
397 // fieldless typed enum. For every arm in [`Semantic::ALL`], the
398 // paired [`std::fmt::Display`] impl + [`AsRef<str>`] impl + the
399 // substrate-canonical [`Semantic::as_str`] `pub const fn`
400 // scalar accessor must resolve to the same `&'static str`
401 // per arm. Peer of the sibling three-path-convergence pins the
402 // substrate carries on the closed-set typed enums the prior
403 // lifts converged onto
404 // (`restart_strategy_display_routes_through_as_str_helper` /
405 // `restart_strategy_as_ref_str_routes_through_as_str_accessor`
406 // on [`caixa_core::supervisor::RestartStrategy`],
407 // `ferrite_runtime_display_and_as_ref_str_route_through_variant_slug_accessor`
408 // on `caixa_provedor::FerriteRuntime`, and the analogous pins
409 // on `caixa_lint::Severity` / `caixa_lint::FixSafety` /
410 // `caixa_arch::InvariantKind` / `caixa_arch::ArchVerdict`).
411 //
412 // A future accidental split (a hand-rolled `impl fmt::Display`
413 // that shadows this route through a divergent per-arm match, an
414 // `impl AsRef<str>` that returns the compiler-derived `Debug`
415 // string via `format!("{:?}", self)` — allocating and diverging
416 // on every arm — or a `#[serde(rename_all = "…")]` attribute
417 // drift that quietly forks the projection) trips this pin at
418 // caixa-theme build time rather than surfacing as a downstream
419 // consumer's silently-forked per-Semantic dispatch far from the
420 // trait-impl declaration site.
421 for &sem in Semantic::ALL {
422 let via_as_str: &str = sem.as_str();
423 let via_display: String = format!("{sem}");
424 let via_as_ref: &str = <Semantic as AsRef<str>>::as_ref(&sem);
425 assert_eq!(
426 via_display, via_as_str,
427 "Semantic::{sem:?} — Display routes off `as_str`; got \
428 Display={via_display:?} vs as_str={via_as_str:?}",
429 );
430 assert_eq!(
431 via_as_ref, via_as_str,
432 "Semantic::{sem:?} — AsRef<str> routes off `as_str`; got \
433 AsRef={via_as_ref:?} vs as_str={via_as_str:?}",
434 );
435 }
436 }
437
438 #[test]
439 fn semantic_as_str_is_usable_in_const_context() {
440 // The [`Semantic::as_str`] accessor is declared `pub const fn`,
441 // matching the peer closed-set typed enums' canonical
442 // `&'static str` projection accessors
443 // ([`caixa_core::CaixaKind::as_str`],
444 // [`caixa_core::supervisor::RestartStrategy::as_str`],
445 // [`caixa_core::aplicacao::PlacementStrategy::as_str`],
446 // `caixa_provedor::FerriteRuntime::variant_slug`). Pin the
447 // same posture with a `const {}` assertion block so a future
448 // accidental downgrade to non-`const` (an added runtime helper
449 // reachable only from a non-`const` context) trips at
450 // caixa-theme build time rather than surfacing as a downstream
451 // `const`-context regression far from the accessor
452 // declaration.
453 const KEYWORD: &str = Semantic::Keyword.as_str();
454 const ERROR: &str = Semantic::Error.as_str();
455 const UNCHANGED: &str = Semantic::Unchanged.as_str();
456 const { assert!(KEYWORD.as_bytes()[0] == b'k') };
457 const { assert!(ERROR.as_bytes()[0] == b'e') };
458 const { assert!(UNCHANGED.as_bytes()[0] == b'u') };
459 }
460
461 #[test]
462 fn semantic_is_variant_predicates_are_const_fn() {
463 // The [`gen_platform::IsVariant`] derive emits `const fn`
464 // predicates on the peer [`caixa_core::CaixaKind`] /
465 // [`caixa_core::upgrade::UpgradeInstruction`] /
466 // [`caixa_core::supervisor::RestartStrategy`] /
467 // [`caixa_core::supervisor::RestartPolicy`] closed-set typed
468 // enums — pin the same posture on [`Semantic`] so a future
469 // accidental downgrade to non-`const` (an added runtime helper
470 // reachable only from a non-`const` context, a manual hand-
471 // rolled `impl` that shadows the derive-generated method)
472 // trips at caixa-theme build time rather than surfacing as a
473 // downstream `const`-context regression far from the derive
474 // declaration.
475 const { assert!(Semantic::Keyword.is_keyword()) };
476 const { assert!(Semantic::Error.is_error()) };
477 const { assert!(Semantic::Added.is_added()) };
478 const { assert!(Semantic::Unchanged.is_unchanged()) };
479 }
480}