Skip to main content

mathtex_engine/
platform.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3use core::cell::RefCell;
4
5/// Host runtime services that must remain separate from engine semantics.
6pub trait Platform {
7    /// Returns the diagnostic sink for this platform.
8    fn diagnostics(&self) -> &dyn DiagnosticSink;
9
10    /// Returns host resource limits, defaulting to `HostLimits::default()`.
11    fn limits(&self) -> HostLimits {
12        HostLimits::default()
13    }
14
15    /// Deterministic clock value visible to generated TeX code.
16    fn clock(&self) -> HostClock {
17        HostClock::default()
18    }
19
20    /// Called before the engine begins line break iteration for a paragraph.
21    fn linebreak_start(&self, _request: LinebreakRequest<'_>) {}
22
23    /// Returns the next line break position, or `None` to use the built in algorithm.
24    fn linebreak_next(&self) -> Option<i32> {
25        None
26    }
27}
28
29impl<T> Platform for &T
30where
31    T: Platform,
32{
33    fn diagnostics(&self) -> &dyn DiagnosticSink {
34        (*self).diagnostics()
35    }
36
37    fn limits(&self) -> HostLimits {
38        (*self).limits()
39    }
40
41    fn clock(&self) -> HostClock {
42        (*self).clock()
43    }
44
45    fn linebreak_start(&self, request: LinebreakRequest<'_>) {
46        (*self).linebreak_start(request);
47    }
48
49    fn linebreak_next(&self) -> Option<i32> {
50        (*self).linebreak_next()
51    }
52}
53
54/// Receiver for diagnostic messages emitted during engine execution.
55pub trait DiagnosticSink {
56    /// Accepts and handles a single diagnostic message.
57    fn emit(&self, diagnostic: Diagnostic);
58}
59
60/// A single diagnostic message emitted by the engine.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct Diagnostic {
63    /// Severity level of the diagnostic.
64    pub severity: DiagnosticSeverity,
65    /// Human readable diagnostic text.
66    pub message: String,
67}
68
69/// Severity level of a diagnostic message.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71#[non_exhaustive]
72pub enum DiagnosticSeverity {
73    /// Informational message; no action required.
74    Info,
75    /// Warning that may indicate a problem but does not stop rendering.
76    Warning,
77    /// Error that prevented successful rendering.
78    Error,
79}
80
81/// Host imposed upper bounds on engine resource consumption.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub struct HostLimits {
84    /// Maximum input bytes accepted for one fragment.
85    pub max_input_bytes: usize,
86    /// Maximum resource requests made while executing one fragment.
87    pub max_resource_requests: usize,
88    /// Maximum layout nodes an engine session should emit.
89    pub max_layout_nodes: usize,
90}
91
92impl Default for HostLimits {
93    fn default() -> Self {
94        Self {
95            max_input_bytes: 1 << 20,
96            max_resource_requests: 256,
97            max_layout_nodes: 1 << 20,
98        }
99    }
100}
101
102/// Host clock value in seconds and microseconds.
103#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
104pub struct HostClock {
105    /// Seconds since the host defined epoch.
106    pub seconds: i32,
107    /// Microseconds within the current second.
108    pub micros: i32,
109}
110
111/// Parameters for a host driven line break request passed to the platform.
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub struct LinebreakRequest<'a> {
114    /// Generated engine font identifier active for the text.
115    pub font: i32,
116    /// Generated engine locale identifier active for the text.
117    pub locale: i32,
118    /// Text slice owned by generated engine memory for this call.
119    pub text: &'a [u16],
120}
121
122/// Signals that a host configured resource limit was exceeded.
123#[derive(Clone, Debug, PartialEq, Eq)]
124#[non_exhaustive]
125pub enum LimitError {
126    /// Input byte count exceeded the configured maximum.
127    InputTooLarge {
128        /// Actual byte count seen.
129        actual: usize,
130        /// Configured byte limit.
131        limit: usize,
132    },
133    /// Resource request count exceeded the configured maximum.
134    TooManyResourceRequests {
135        /// Actual request count.
136        actual: usize,
137        /// Configured request limit.
138        limit: usize,
139    },
140    /// Layout node count exceeded the configured maximum.
141    TooManyLayoutNodes {
142        /// Actual node count.
143        actual: usize,
144        /// Configured node limit.
145        limit: usize,
146    },
147}
148
149/// Diagnostic sink that discards all messages.
150#[derive(Clone, Copy, Debug, Default)]
151pub struct NoopDiagnosticSink;
152
153impl DiagnosticSink for NoopDiagnosticSink {
154    fn emit(&self, _diagnostic: Diagnostic) {}
155}
156
157/// Diagnostic sink that accumulates messages, usable without std.
158#[derive(Debug, Default)]
159pub struct CollectingDiagnosticSink {
160    diagnostics: RefCell<Vec<Diagnostic>>,
161}
162
163impl CollectingDiagnosticSink {
164    /// Creates an empty collecting sink.
165    #[must_use]
166    pub fn new() -> Self {
167        Self::default()
168    }
169
170    /// Returns a copy of all accumulated diagnostics without consuming them.
171    #[must_use]
172    pub fn snapshot(&self) -> Vec<Diagnostic> {
173        self.diagnostics.borrow().clone()
174    }
175
176    /// Removes and returns all accumulated diagnostics.
177    #[must_use]
178    pub fn drain(&self) -> Vec<Diagnostic> {
179        self.diagnostics.borrow_mut().drain(..).collect()
180    }
181
182    /// Returns the number of accumulated diagnostics.
183    #[must_use]
184    pub fn len(&self) -> usize {
185        self.diagnostics.borrow().len()
186    }
187
188    /// Returns true when no diagnostics have been collected.
189    #[must_use]
190    pub fn is_empty(&self) -> bool {
191        self.diagnostics.borrow().is_empty()
192    }
193
194    /// Discards all accumulated diagnostics.
195    pub fn clear(&self) {
196        self.diagnostics.borrow_mut().clear();
197    }
198}
199
200impl DiagnosticSink for CollectingDiagnosticSink {
201    fn emit(&self, diagnostic: Diagnostic) {
202        self.diagnostics.borrow_mut().push(diagnostic);
203    }
204}
205
206/// Platform implementation composed from a diagnostic sink, host limits, and a clock.
207#[derive(Clone, Debug)]
208pub struct ConfigurablePlatform<D> {
209    diagnostics: D,
210    limits: HostLimits,
211    clock: HostClock,
212}
213
214impl<D> ConfigurablePlatform<D> {
215    /// Creates a platform with the given diagnostic sink and limits.
216    #[must_use]
217    pub fn new(diagnostics: D, limits: HostLimits) -> Self {
218        Self {
219            diagnostics,
220            limits,
221            clock: HostClock::default(),
222        }
223    }
224
225    /// Creates a platform with the given diagnostic sink and default limits.
226    #[must_use]
227    pub fn with_diagnostics(diagnostics: D) -> Self {
228        Self {
229            diagnostics,
230            limits: HostLimits::default(),
231            clock: HostClock::default(),
232        }
233    }
234
235    /// Returns a copy of this platform with the clock set to `clock`.
236    #[must_use]
237    pub fn with_clock(mut self, clock: HostClock) -> Self {
238        self.clock = clock;
239        self
240    }
241
242    /// Returns a reference to the underlying diagnostic sink.
243    #[must_use]
244    pub fn diagnostic_sink(&self) -> &D {
245        &self.diagnostics
246    }
247
248    /// Returns the configured host limits.
249    #[must_use]
250    pub fn host_limits(&self) -> HostLimits {
251        self.limits
252    }
253
254    /// Returns the configured host clock.
255    #[must_use]
256    pub fn host_clock(&self) -> HostClock {
257        self.clock
258    }
259}
260
261impl<D> Platform for ConfigurablePlatform<D>
262where
263    D: DiagnosticSink,
264{
265    fn diagnostics(&self) -> &dyn DiagnosticSink {
266        &self.diagnostics
267    }
268
269    fn limits(&self) -> HostLimits {
270        self.limits
271    }
272
273    fn clock(&self) -> HostClock {
274        self.clock
275    }
276}
277
278/// Minimal deterministic platform for tests, embedded use, and early bootstrap.
279#[derive(Clone, Copy, Debug, Default)]
280pub struct NoopPlatform {
281    diagnostics: NoopDiagnosticSink,
282    limits: HostLimits,
283    clock: HostClock,
284}
285
286impl NoopPlatform {
287    /// Creates a noop platform with the given host limits.
288    #[must_use]
289    pub fn with_limits(limits: HostLimits) -> Self {
290        Self {
291            diagnostics: NoopDiagnosticSink,
292            limits,
293            clock: HostClock::default(),
294        }
295    }
296
297    /// Creates a noop platform with the given clock value.
298    #[must_use]
299    pub fn with_clock(clock: HostClock) -> Self {
300        Self {
301            diagnostics: NoopDiagnosticSink,
302            limits: HostLimits::default(),
303            clock,
304        }
305    }
306}
307
308impl Platform for NoopPlatform {
309    fn diagnostics(&self) -> &dyn DiagnosticSink {
310        &self.diagnostics
311    }
312
313    fn limits(&self) -> HostLimits {
314        self.limits
315    }
316
317    fn clock(&self) -> HostClock {
318        self.clock
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn collecting_diagnostic_sink_records_snapshots_and_drains() {
328        let sink = CollectingDiagnosticSink::new();
329
330        sink.emit(Diagnostic {
331            severity: DiagnosticSeverity::Warning,
332            message: "missing glyph".to_string(),
333        });
334
335        assert_eq!(sink.len(), 1);
336        assert_eq!(sink.snapshot()[0].message, "missing glyph");
337        assert_eq!(sink.drain()[0].severity, DiagnosticSeverity::Warning);
338        assert!(sink.is_empty());
339    }
340
341}