Skip to main content

smelt_stdlib/
category.rs

1//! Diagnostic categories shared across the TypeScript and Python frontends.
2//!
3//! The category is decided at the point a diagnostic is raised, where the
4//! frontend knows *why* lowering failed — a missing builtin, an unlowered
5//! construct, a typed-subset violation, and so on. Tooling (the library
6//! probes, a future LSP) consumes the category instead of re-deriving it by
7//! pattern-matching on human-readable message text.
8
9use serde::{Deserialize, Serialize};
10
11/// Coarse, machine-stable classification of a Smelt frontend diagnostic.
12///
13/// This is orthogonal to the per-site `code`/`message`: many distinct codes map
14/// to the same category. Reports group by category to answer "is this blocked
15/// on missing stdlib coverage or on an unimplemented lowering?".
16#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
17#[non_exhaustive]
18#[serde(rename_all = "kebab-case")]
19pub enum DiagnosticCategory {
20    /// Source text could not be parsed.
21    Parse,
22    /// A known JS/Python builtin or stdlib API that Smelt recognizes but has
23    /// not implemented lowering or runtime support for yet.
24    MissingStdlib,
25    /// A language construct Smelt does not lower into Rust yet.
26    UnsupportedLowering,
27    /// Source violates Smelt's strict typed-subset requirements, such as a
28    /// missing required type annotation. The fix is in the source, not Smelt.
29    TypeConstraint,
30    /// A name could not be resolved to any declaration or known builtin.
31    UnresolvedReference,
32    /// Internal invariant violation or a not-yet-classified diagnostic.
33    Internal,
34}
35
36impl DiagnosticCategory {
37    /// Stable kebab-case identifier, e.g. for JSON output or report grouping.
38    #[must_use]
39    pub const fn as_str(self) -> &'static str {
40        match self {
41            Self::Parse => "parse",
42            Self::MissingStdlib => "missing-stdlib",
43            Self::UnsupportedLowering => "unsupported-lowering",
44            Self::TypeConstraint => "type-constraint",
45            Self::UnresolvedReference => "unresolved-reference",
46            Self::Internal => "internal",
47        }
48    }
49
50    /// Short human-readable label for report tables.
51    #[must_use]
52    pub const fn label(self) -> &'static str {
53        match self {
54            Self::Parse => "parse error",
55            Self::MissingStdlib => "missing stdlib",
56            Self::UnsupportedLowering => "non-working Rust (unlowered)",
57            Self::TypeConstraint => "typed-subset violation",
58            Self::UnresolvedReference => "unresolved reference",
59            Self::Internal => "internal",
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    /// Every category round-trips through its stable string identifier.
69    #[test]
70    fn as_str_is_stable_kebab_case() {
71        assert_eq!(DiagnosticCategory::MissingStdlib.as_str(), "missing-stdlib");
72        assert_eq!(
73            DiagnosticCategory::UnsupportedLowering.as_str(),
74            "unsupported-lowering"
75        );
76        assert_eq!(DiagnosticCategory::Parse.as_str(), "parse");
77    }
78
79    /// Serde uses the same kebab-case spelling as `as_str`.
80    #[test]
81    fn serde_matches_as_str() -> Result<(), serde_json::Error> {
82        let json = serde_json::to_string(&DiagnosticCategory::TypeConstraint)?;
83        assert_eq!(json, "\"type-constraint\"");
84        let back: DiagnosticCategory = serde_json::from_str(&json)?;
85        assert_eq!(back, DiagnosticCategory::TypeConstraint);
86        Ok(())
87    }
88}