Skip to main content

fmd_math/
error.rs

1//! The error contract: precise, named, tier-tagged failures.
2//!
3//! The doctrine (franken_manim §11.5, R1): an unsupported construct is a
4//! **precise, named error** — never silence, never garbage — and arbitrary
5//! token streams error cleanly, never hang or garble. The `Display` formats
6//! here are **standardized**: the public coverage ratchet parses construct
7//! names out of [`MathError::UnsupportedCommand`] messages, so the message
8//! shapes below are a stable contract locked by tests:
9//!
10//! - tier-2:   `` `\substack` is not yet supported; tier T2, tracked at … ``
11//! - untiered: `` `\foo` is not supported; untiered (not observed in the
12//!   G0-4 corpus), report at … ``
13//! - malformed: `malformed mathematics at byte N: …`
14//! - unmapped:  `` character 'x' (U+0078) has no glyph in the bundled math
15//!   faces … ``
16
17use crate::commands::{
18    ConstructStatus, LAYOUT_PENDING_TRACKING, TIER2_TRACKING, UNTIERED_TRACKING, construct_status,
19};
20use crate::node::Span;
21
22/// Why a source string failed to parse (or, later, to lay out).
23///
24/// The shape is frozen by the G0-3 ratification note: three variants, with
25/// the construct name of an unsupported command carried verbatim in the
26/// construct table's naming scheme (`\substack`, `env:flushleft`, …).
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum MathError {
29    /// A construct outside the implemented surface. `name` is in the G0-4
30    /// construct-table scheme (`\substack`, `env:flushleft`); the tier tag
31    /// in the rendered message comes from the registry.
32    UnsupportedCommand {
33        /// Construct name, table scheme.
34        name: String,
35        /// Source span of the offending construct.
36        span: Span,
37    },
38    /// Structurally invalid input (unbalanced braces, a double superscript,
39    /// `\right` without `\left`, …).
40    Malformed {
41        /// What is wrong, in one clause.
42        what: String,
43        /// Byte offset of the offense.
44        at: usize,
45    },
46    /// A character with no glyph in the bundled math faces (a layout-stage
47    /// error; the parser accepts every character).
48    UnmappedChar {
49        /// The character.
50        ch: char,
51        /// Its source span.
52        span: Span,
53    },
54}
55
56impl MathError {
57    /// The byte span the error points at.
58    #[must_use]
59    pub fn span(&self) -> Span {
60        match self {
61            Self::UnsupportedCommand { span, .. } | Self::UnmappedChar { span, .. } => *span,
62            Self::Malformed { at, .. } => Span::new(*at, *at),
63        }
64    }
65
66    /// For [`MathError::UnsupportedCommand`], the construct name in the
67    /// construct-table scheme; the ratchet counts by this.
68    #[must_use]
69    pub fn unsupported_construct(&self) -> Option<&str> {
70        match self {
71            Self::UnsupportedCommand { name, .. } => Some(name),
72            _ => None,
73        }
74    }
75}
76
77impl core::fmt::Display for MathError {
78    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79        match self {
80            Self::UnsupportedCommand { name, span } => match construct_status(name) {
81                ConstructStatus::UnsupportedT2 => write!(
82                    f,
83                    "`{name}` is not yet supported; tier T2, tracked at {TIER2_TRACKING} \
84                     (bytes {}..{})",
85                    span.start, span.end
86                ),
87                ConstructStatus::Supported => write!(
88                    f,
89                    "`{name}` parses, but its layout is not yet implemented; tracked at \
90                     {LAYOUT_PENDING_TRACKING} (bytes {}..{})",
91                    span.start, span.end
92                ),
93                ConstructStatus::Unknown => write!(
94                    f,
95                    "`{name}` is not supported; untiered (not observed in the G0-4 corpus), \
96                     report at {UNTIERED_TRACKING} (bytes {}..{})",
97                    span.start, span.end
98                ),
99            },
100            Self::Malformed { what, at } => {
101                write!(f, "malformed mathematics at byte {at}: {what}")
102            }
103            Self::UnmappedChar { ch, span } => write!(
104                f,
105                "character '{ch}' (U+{:04X}) has no glyph in the bundled math faces \
106                 (bytes {}..{})",
107                *ch as u32, span.start, span.end
108            ),
109        }
110    }
111}
112
113impl std::error::Error for MathError {}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn tier2_message_format_is_stable() {
121        // The example must stay a construct the T2 vocabulary still names
122        // as pending (`\substack` and `\dddot` graduated).
123        // `\dx` is the last pending construct — a 3b1b preamble macro
124        // awaiting the corpus preamble evidence.
125        let err = MathError::UnsupportedCommand {
126            name: r"\dx".to_owned(),
127            span: Span::new(3, 6),
128        };
129        assert_eq!(
130            err.to_string(),
131            "`\\dx` is not yet supported; tier T2, tracked at franken_manim fm-j5t \
132             (the tier-2 construct program) (bytes 3..6)"
133        );
134    }
135
136    #[test]
137    fn untiered_message_format_is_stable() {
138        let err = MathError::UnsupportedCommand {
139            name: r"\notacommand".to_owned(),
140            span: Span::new(0, 12),
141        };
142        assert_eq!(
143            err.to_string(),
144            "`\\notacommand` is not supported; untiered (not observed in the G0-4 corpus), \
145             report at https://github.com/Dicklesworthstone/franken_manim/issues (bytes 0..12)"
146        );
147    }
148
149    #[test]
150    fn malformed_message_format_is_stable() {
151        let err = MathError::Malformed {
152            what: "unmatched '}'".to_owned(),
153            at: 7,
154        };
155        assert_eq!(
156            err.to_string(),
157            "malformed mathematics at byte 7: unmatched '}'"
158        );
159    }
160}