Skip to main content

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}