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    /// Returns the box for a host owned token, or `None` to typeset a zero size box.
29    fn host_box(&self, _request: HostBoxRequest) -> Option<HostBox> {
30        None
31    }
32}
33
34impl<T> Platform for &T
35where
36    T: Platform,
37{
38    fn diagnostics(&self) -> &dyn DiagnosticSink {
39        (*self).diagnostics()
40    }
41
42    fn limits(&self) -> HostLimits {
43        (*self).limits()
44    }
45
46    fn clock(&self) -> HostClock {
47        (*self).clock()
48    }
49
50    fn linebreak_start(&self, request: LinebreakRequest<'_>) {
51        (*self).linebreak_start(request);
52    }
53
54    fn linebreak_next(&self) -> Option<i32> {
55        (*self).linebreak_next()
56    }
57
58    fn host_box(&self, request: HostBoxRequest) -> Option<HostBox> {
59        (*self).host_box(request)
60    }
61}
62
63/// Receiver for diagnostic messages emitted during engine execution.
64pub trait DiagnosticSink {
65    /// Accepts and handles a single diagnostic message.
66    fn emit(&self, diagnostic: Diagnostic);
67}
68
69/// A single diagnostic message emitted by the engine.
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct Diagnostic {
72    /// Severity level of the diagnostic.
73    pub severity: DiagnosticSeverity,
74    /// Human readable diagnostic text.
75    pub message: String,
76}
77
78/// Severity level of a diagnostic message.
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80#[non_exhaustive]
81pub enum DiagnosticSeverity {
82    /// Informational message, no action required.
83    Info,
84    /// Warning that may indicate a problem but does not stop rendering.
85    Warning,
86    /// Error that prevented successful rendering.
87    Error,
88}
89
90/// Host imposed upper bounds on engine resource consumption.
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub struct HostLimits {
93    /// Maximum input bytes accepted for one fragment.
94    pub max_input_bytes: usize,
95    /// Maximum resource requests made while executing one fragment.
96    pub max_resource_requests: usize,
97    /// Maximum layout nodes an engine session should emit.
98    pub max_layout_nodes: usize,
99}
100
101impl Default for HostLimits {
102    fn default() -> Self {
103        Self {
104            max_input_bytes: 1 << 20,
105            max_resource_requests: 256,
106            max_layout_nodes: 1 << 20,
107        }
108    }
109}
110
111/// Host clock value in seconds and microseconds.
112#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
113pub struct HostClock {
114    /// Seconds since the host defined epoch.
115    pub seconds: i32,
116    /// Microseconds within the current second.
117    pub micros: i32,
118}
119
120/// Parameters for a host driven line break request passed to the platform.
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub struct LinebreakRequest<'a> {
123    /// Generated engine font identifier active for the text.
124    pub font: i32,
125    /// Generated engine locale identifier active for the text.
126    pub locale: i32,
127    /// Text slice owned by generated engine memory for this call.
128    pub text: &'a [u16],
129}
130
131/// Style context a host box is placed in, display math collapses to text.
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub enum HostBoxStyle {
134    /// Text and display math style.
135    Text,
136    /// First level script style.
137    Script,
138    /// Second level script style.
139    ScriptScript,
140}
141
142/// Parameters for a host box request passed to the platform.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct HostBoxRequest {
145    /// Opaque host owned token identifier scanned from `\hostbox{...}`.
146    pub token: i32,
147    /// Math style context at the placement site.
148    pub style: HostBoxStyle,
149    /// Font size context in scaled points at the placement site.
150    pub font_size: i32,
151}
152
153/// A glyph inside a host box, offsets in scaled points from the box baseline origin, y down.
154#[derive(Clone, Copy, Debug, PartialEq, Eq)]
155pub struct HostBoxGlyph {
156    /// Glyph identifier in the run's font.
157    pub glyph: u16,
158    /// Horizontal offset from the box baseline origin.
159    pub x: i32,
160    /// Vertical offset from the baseline, positive downward.
161    pub y: i32,
162    /// Horizontal advance in scaled points.
163    pub advance: i32,
164}
165
166/// One same font glyph sequence inside a host box.
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub struct HostBoxRun {
169    /// Host font name carried into the fragment's glyph run.
170    pub font_name: String,
171    /// Font size for the run in scaled points.
172    pub font_size: i32,
173    /// Glyphs of the run in visual order.
174    pub glyphs: Vec<HostBoxGlyph>,
175}
176
177/// A rule inside a host box, `(x, y)` is the bottom left corner, y down from the baseline.
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub struct HostBoxRule {
180    /// Horizontal offset of the rule's left edge from the box baseline origin.
181    pub x: i32,
182    /// Vertical offset of the rule's bottom edge, positive downward.
183    pub y: i32,
184    /// Rule width in scaled points.
185    pub width: i32,
186    /// Rule height in scaled points, extending upward from `y`.
187    pub height: i32,
188}
189
190/// Host supplied box for one `\hostbox` token: metrics plus render data for the fragment.
191#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct HostBox {
193    /// Box width in scaled points.
194    pub width: i32,
195    /// Box height above the baseline in scaled points.
196    pub height: i32,
197    /// Box depth below the baseline in scaled points.
198    pub depth: i32,
199    /// Glyph runs rendered inside the box.
200    pub runs: Vec<HostBoxRun>,
201    /// Rules rendered inside the box.
202    pub rules: Vec<HostBoxRule>,
203}
204
205/// Signals that a host configured resource limit was exceeded.
206#[derive(Clone, Debug, PartialEq, Eq)]
207#[non_exhaustive]
208pub enum LimitError {
209    /// Input byte count exceeded the configured maximum.
210    InputTooLarge {
211        /// Actual byte count seen.
212        actual: usize,
213        /// Configured byte limit.
214        limit: usize,
215    },
216    /// Resource request count exceeded the configured maximum.
217    TooManyResourceRequests {
218        /// Actual request count.
219        actual: usize,
220        /// Configured request limit.
221        limit: usize,
222    },
223    /// Layout node count exceeded the configured maximum.
224    TooManyLayoutNodes {
225        /// Actual node count.
226        actual: usize,
227        /// Configured node limit.
228        limit: usize,
229    },
230}
231
232/// Diagnostic sink that discards all messages.
233#[derive(Clone, Copy, Debug, Default)]
234pub struct NoopDiagnosticSink;
235
236impl DiagnosticSink for NoopDiagnosticSink {
237    fn emit(&self, _diagnostic: Diagnostic) {}
238}
239
240/// Diagnostic sink that accumulates messages, usable without std.
241#[derive(Debug, Default)]
242pub struct CollectingDiagnosticSink {
243    diagnostics: RefCell<Vec<Diagnostic>>,
244}
245
246impl CollectingDiagnosticSink {
247    /// Creates an empty collecting sink.
248    #[must_use]
249    pub fn new() -> Self {
250        Self::default()
251    }
252
253    /// Returns a copy of all accumulated diagnostics without consuming them.
254    #[must_use]
255    pub fn snapshot(&self) -> Vec<Diagnostic> {
256        self.diagnostics.borrow().clone()
257    }
258
259    /// Removes and returns all accumulated diagnostics.
260    #[must_use]
261    pub fn drain(&self) -> Vec<Diagnostic> {
262        self.diagnostics.borrow_mut().drain(..).collect()
263    }
264
265    /// Returns the number of accumulated diagnostics.
266    #[must_use]
267    pub fn len(&self) -> usize {
268        self.diagnostics.borrow().len()
269    }
270
271    /// Returns true when no diagnostics have been collected.
272    #[must_use]
273    pub fn is_empty(&self) -> bool {
274        self.diagnostics.borrow().is_empty()
275    }
276
277    /// Discards all accumulated diagnostics.
278    pub fn clear(&self) {
279        self.diagnostics.borrow_mut().clear();
280    }
281}
282
283impl DiagnosticSink for CollectingDiagnosticSink {
284    fn emit(&self, diagnostic: Diagnostic) {
285        self.diagnostics.borrow_mut().push(diagnostic);
286    }
287}
288
289/// Platform implementation composed from a diagnostic sink, host limits, and a clock.
290#[derive(Clone, Debug)]
291pub struct ConfigurablePlatform<D> {
292    diagnostics: D,
293    limits: HostLimits,
294    clock: HostClock,
295}
296
297impl<D> ConfigurablePlatform<D> {
298    /// Creates a platform with the given diagnostic sink and limits.
299    #[must_use]
300    pub fn new(diagnostics: D, limits: HostLimits) -> Self {
301        Self {
302            diagnostics,
303            limits,
304            clock: HostClock::default(),
305        }
306    }
307
308    /// Creates a platform with the given diagnostic sink and default limits.
309    #[must_use]
310    pub fn with_diagnostics(diagnostics: D) -> Self {
311        Self {
312            diagnostics,
313            limits: HostLimits::default(),
314            clock: HostClock::default(),
315        }
316    }
317
318    /// Returns a copy of this platform with the clock set to `clock`.
319    #[must_use]
320    pub fn with_clock(mut self, clock: HostClock) -> Self {
321        self.clock = clock;
322        self
323    }
324
325    /// Returns a reference to the underlying diagnostic sink.
326    #[must_use]
327    pub fn diagnostic_sink(&self) -> &D {
328        &self.diagnostics
329    }
330
331    /// Returns the configured host limits.
332    #[must_use]
333    pub fn host_limits(&self) -> HostLimits {
334        self.limits
335    }
336
337    /// Returns the configured host clock.
338    #[must_use]
339    pub fn host_clock(&self) -> HostClock {
340        self.clock
341    }
342}
343
344impl<D> Platform for ConfigurablePlatform<D>
345where
346    D: DiagnosticSink,
347{
348    fn diagnostics(&self) -> &dyn DiagnosticSink {
349        &self.diagnostics
350    }
351
352    fn limits(&self) -> HostLimits {
353        self.limits
354    }
355
356    fn clock(&self) -> HostClock {
357        self.clock
358    }
359}
360
361/// Minimal deterministic platform for tests, embedded use, and early bootstrap.
362#[derive(Clone, Copy, Debug, Default)]
363pub struct NoopPlatform {
364    diagnostics: NoopDiagnosticSink,
365    limits: HostLimits,
366    clock: HostClock,
367}
368
369impl NoopPlatform {
370    /// Creates a noop platform with the given host limits.
371    #[must_use]
372    pub fn with_limits(limits: HostLimits) -> Self {
373        Self {
374            diagnostics: NoopDiagnosticSink,
375            limits,
376            clock: HostClock::default(),
377        }
378    }
379
380    /// Creates a noop platform with the given clock value.
381    #[must_use]
382    pub fn with_clock(clock: HostClock) -> Self {
383        Self {
384            diagnostics: NoopDiagnosticSink,
385            limits: HostLimits::default(),
386            clock,
387        }
388    }
389}
390
391impl Platform for NoopPlatform {
392    fn diagnostics(&self) -> &dyn DiagnosticSink {
393        &self.diagnostics
394    }
395
396    fn limits(&self) -> HostLimits {
397        self.limits
398    }
399
400    fn clock(&self) -> HostClock {
401        self.clock
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn collecting_diagnostic_sink_records_snapshots_and_drains() {
411        let sink = CollectingDiagnosticSink::new();
412
413        sink.emit(Diagnostic {
414            severity: DiagnosticSeverity::Warning,
415            message: "missing glyph".to_string(),
416        });
417
418        assert_eq!(sink.len(), 1);
419        assert_eq!(sink.snapshot()[0].message, "missing glyph");
420        assert_eq!(sink.drain()[0].severity, DiagnosticSeverity::Warning);
421        assert!(sink.is_empty());
422    }
423}