use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, gen_platform::IsVariant,
)]
pub enum Semantic {
Keyword,
Symbol,
KeywordArg,
String,
Number,
Literal,
Comment,
Accent,
Muted,
Error,
Warning,
Info,
Hint,
Added,
Removed,
Unchanged,
}
impl Semantic {
pub const ALL: &'static [Self] = &[
Self::Keyword,
Self::Symbol,
Self::KeywordArg,
Self::String,
Self::Number,
Self::Literal,
Self::Comment,
Self::Accent,
Self::Muted,
Self::Error,
Self::Warning,
Self::Info,
Self::Hint,
Self::Added,
Self::Removed,
Self::Unchanged,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Keyword => "keyword",
Self::Symbol => "symbol",
Self::KeywordArg => "keyword-arg",
Self::String => "string",
Self::Number => "number",
Self::Literal => "literal",
Self::Comment => "comment",
Self::Accent => "accent",
Self::Muted => "muted",
Self::Error => "error",
Self::Warning => "warning",
Self::Info => "info",
Self::Hint => "hint",
Self::Added => "added",
Self::Removed => "removed",
Self::Unchanged => "unchanged",
}
}
#[must_use]
pub fn from_wire(s: &str) -> Option<Self> {
match s {
"keyword" => Some(Self::Keyword),
"symbol" => Some(Self::Symbol),
"keyword-arg" => Some(Self::KeywordArg),
"string" => Some(Self::String),
"number" => Some(Self::Number),
"literal" => Some(Self::Literal),
"comment" => Some(Self::Comment),
"accent" => Some(Self::Accent),
"muted" => Some(Self::Muted),
"error" => Some(Self::Error),
"warning" => Some(Self::Warning),
"info" => Some(Self::Info),
"hint" => Some(Self::Hint),
"added" => Some(Self::Added),
"removed" => Some(Self::Removed),
"unchanged" => Some(Self::Unchanged),
_ => None,
}
}
}
impl std::fmt::Display for Semantic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for Semantic {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semantic_all_enumerates_every_variant_in_declaration_order() {
assert_eq!(
Semantic::ALL,
&[
Semantic::Keyword,
Semantic::Symbol,
Semantic::KeywordArg,
Semantic::String,
Semantic::Number,
Semantic::Literal,
Semantic::Comment,
Semantic::Accent,
Semantic::Muted,
Semantic::Error,
Semantic::Warning,
Semantic::Info,
Semantic::Hint,
Semantic::Added,
Semantic::Removed,
Semantic::Unchanged,
],
);
for variant in Semantic::ALL {
let row = [
variant.is_keyword(),
variant.is_symbol(),
variant.is_keyword_arg(),
variant.is_string(),
variant.is_number(),
variant.is_literal(),
variant.is_comment(),
variant.is_accent(),
variant.is_muted(),
variant.is_error(),
variant.is_warning(),
variant.is_info(),
variant.is_hint(),
variant.is_added(),
variant.is_removed(),
variant.is_unchanged(),
];
let hits = row.iter().filter(|b| **b).count();
assert_eq!(
hits, 1,
"Semantic::{variant:?} must satisfy exactly one of the \
15 is_* arm-discriminator predicates; got {row:?}",
);
}
}
#[test]
fn semantic_is_variant_predicates_partition_the_arm_set() {
for (idx, variant) in Semantic::ALL.iter().enumerate() {
let observed: [bool; 16] = [
variant.is_keyword(),
variant.is_symbol(),
variant.is_keyword_arg(),
variant.is_string(),
variant.is_number(),
variant.is_literal(),
variant.is_comment(),
variant.is_accent(),
variant.is_muted(),
variant.is_error(),
variant.is_warning(),
variant.is_info(),
variant.is_hint(),
variant.is_added(),
variant.is_removed(),
variant.is_unchanged(),
];
let mut expected = [false; 16];
expected[idx] = true;
assert_eq!(
observed, expected,
"Semantic::{variant:?} at ALL[{idx}] is_* predicates \
must fire only on their own arm lane (identity \
diagonal); got {observed:?}",
);
}
}
#[test]
fn semantic_as_str_returns_canonical_kebab_case_per_arm() {
assert_eq!(Semantic::Keyword.as_str(), "keyword");
assert_eq!(Semantic::Symbol.as_str(), "symbol");
assert_eq!(Semantic::KeywordArg.as_str(), "keyword-arg");
assert_eq!(Semantic::String.as_str(), "string");
assert_eq!(Semantic::Number.as_str(), "number");
assert_eq!(Semantic::Literal.as_str(), "literal");
assert_eq!(Semantic::Comment.as_str(), "comment");
assert_eq!(Semantic::Accent.as_str(), "accent");
assert_eq!(Semantic::Muted.as_str(), "muted");
assert_eq!(Semantic::Error.as_str(), "error");
assert_eq!(Semantic::Warning.as_str(), "warning");
assert_eq!(Semantic::Info.as_str(), "info");
assert_eq!(Semantic::Hint.as_str(), "hint");
assert_eq!(Semantic::Added.as_str(), "added");
assert_eq!(Semantic::Removed.as_str(), "removed");
assert_eq!(Semantic::Unchanged.as_str(), "unchanged");
}
#[test]
fn semantic_as_str_projections_are_all_distinct_across_arms() {
let mut projections: Vec<&'static str> = Semantic::ALL.iter().map(|s| s.as_str()).collect();
let before = projections.len();
projections.sort_unstable();
projections.dedup();
assert_eq!(
projections.len(),
before,
"Semantic::as_str must be injective across ALL — collisions: \
{projections:?}",
);
}
#[test]
fn semantic_display_and_as_ref_str_route_through_as_str_accessor() {
for &sem in Semantic::ALL {
let via_as_str: &str = sem.as_str();
let via_display: String = format!("{sem}");
let via_as_ref: &str = <Semantic as AsRef<str>>::as_ref(&sem);
assert_eq!(
via_display, via_as_str,
"Semantic::{sem:?} — Display routes off `as_str`; got \
Display={via_display:?} vs as_str={via_as_str:?}",
);
assert_eq!(
via_as_ref, via_as_str,
"Semantic::{sem:?} — AsRef<str> routes off `as_str`; got \
AsRef={via_as_ref:?} vs as_str={via_as_str:?}",
);
}
}
#[test]
fn semantic_as_str_is_usable_in_const_context() {
const KEYWORD: &str = Semantic::Keyword.as_str();
const ERROR: &str = Semantic::Error.as_str();
const UNCHANGED: &str = Semantic::Unchanged.as_str();
const { assert!(KEYWORD.as_bytes()[0] == b'k') };
const { assert!(ERROR.as_bytes()[0] == b'e') };
const { assert!(UNCHANGED.as_bytes()[0] == b'u') };
}
#[test]
fn semantic_from_wire_accepts_every_as_str_output() {
for &variant in Semantic::ALL {
let wire = variant.as_str();
let parsed = Semantic::from_wire(wire).unwrap_or_else(|| {
panic!(
"Semantic::from_wire({wire:?}) must accept every \
Semantic::as_str output — got None for the wire \
byte-string of {variant:?}"
)
});
assert_eq!(
parsed, variant,
"Semantic::from_wire(Semantic::{variant:?}.as_str()) \
must return Semantic::{variant:?} — the (as_str, \
from_wire) pair must form a total round-trip on the \
closed 16-arm Semantic arm-set",
);
}
}
#[test]
fn semantic_from_wire_rejects_unknown_byte_strings() {
for bad in [
"",
" ",
"Keyword",
"KEYWORD",
"Symbol",
"SYMBOL",
"KeywordArg",
"keyword_arg",
"keywordarg",
"String",
"STRING",
"Number",
"Literal",
"Comment",
"Accent",
"Muted",
"Error",
"ERROR",
"Warning",
"WARNING",
"Info",
"INFO",
"Hint",
"HINT",
"Added",
"ADDED",
"Removed",
"REMOVED",
"Unchanged",
"UNCHANGED",
"kewyord",
"sym",
"kw",
"str",
"num",
"lit",
"cmt",
"safe",
"unsafe",
"safety",
"compliance",
"proven",
"rejected",
"namespace",
"deleted",
"highlight",
"identifier",
"keyword ",
" keyword",
"keyword\n",
"keyword\t",
"keyword-arg ",
" keyword-arg",
"added ",
" added",
"unchanged ",
" unchanged",
] {
assert!(
Semantic::from_wire(bad).is_none(),
"Semantic::from_wire({bad:?}) must return None — the \
parser's accept-set is exactly the 16 Semantic::as_str \
outputs; a widening would silently split the parser's \
accept-set from the emitter's arm-set",
);
}
}
#[test]
fn semantic_is_variant_predicates_are_const_fn() {
const { assert!(Semantic::Keyword.is_keyword()) };
const { assert!(Semantic::Error.is_error()) };
const { assert!(Semantic::Added.is_added()) };
const { assert!(Semantic::Unchanged.is_unchanged()) };
}
}