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}