Skip to main content

submilli_engine/
rendering.rs

1//! Bounded diagnostic output. Truncation is presentation, not a compiler failure.
2
3use std::fmt;
4
5pub const TRUNCATED: &str = "[diagnostic output truncated]";
6
7#[derive(Clone, Copy, Debug)]
8pub struct RenderLimits {
9    pub bytes: usize,
10    pub steps: usize,
11    pub type_depth: usize,
12}
13
14impl Default for RenderLimits {
15    fn default() -> Self {
16        Self {
17            bytes: 64 * 1024,
18            steps: 65_536,
19            type_depth: 512,
20        }
21    }
22}
23
24impl RenderLimits {
25    pub fn collection() -> Self {
26        Self {
27            bytes: 1024 * 1024,
28            ..Self::default()
29        }
30    }
31}
32
33#[derive(Debug, PartialEq, Eq)]
34pub struct RenderedText {
35    pub text: String,
36    pub truncated: bool,
37    steps: usize,
38}
39
40#[derive(Debug)]
41pub enum RenderError {
42    InvalidMetadata(&'static str),
43    Source(crate::source::SourceError),
44    Allocation,
45    Formatting,
46    /// Private rendering control flow, consumed by `Writer::render`.
47    Truncated,
48}
49
50impl fmt::Display for RenderError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::InvalidMetadata(context) => write!(f, "invalid diagnostic metadata: {context}"),
54            Self::Source(error) => write!(
55                f,
56                "invalid diagnostic metadata; source context unavailable: {error}"
57            ),
58            Self::Allocation => f.write_str("diagnostic allocation failed"),
59            Self::Formatting => f.write_str("diagnostic formatting failed"),
60            Self::Truncated => f.write_str(TRUNCATED),
61        }
62    }
63}
64impl std::error::Error for RenderError {}
65impl From<crate::source::SourceError> for RenderError {
66    fn from(error: crate::source::SourceError) -> Self {
67        Self::Source(error)
68    }
69}
70impl From<RenderError> for crate::compiler_error::CompilerFailure {
71    fn from(error: RenderError) -> Self {
72        Self::Internal {
73            stage: crate::compiler_error::CompilerStage::Infer,
74            span: None,
75            message: error.to_string(),
76        }
77    }
78}
79
80pub(crate) struct Writer {
81    text: String,
82    limits: RenderLimits,
83    steps: usize,
84    failure: Option<RenderError>,
85    abbreviated: bool,
86}
87
88impl RenderedText {
89    pub(crate) fn steps(&self) -> usize {
90        self.steps
91    }
92}
93
94impl Writer {
95    pub(crate) fn remaining_steps(&self) -> usize {
96        self.limits.steps.saturating_sub(self.steps)
97    }
98
99    pub(crate) fn render(
100        limits: RenderLimits,
101        render: impl FnOnce(&mut Self) -> Result<(), RenderError>,
102    ) -> Result<RenderedText, RenderError> {
103        if limits.bytes < TRUNCATED.len() {
104            return Err(RenderError::InvalidMetadata(
105                "render byte limit cannot hold truncation marker",
106            ));
107        }
108        let mut out = Self {
109            text: String::new(),
110            limits,
111            steps: 0,
112            failure: None,
113            abbreviated: false,
114        };
115        let result = render(&mut out);
116        match out.failure.take().map_or(result, Err) {
117            Ok(()) => Ok(RenderedText {
118                text: out.text,
119                truncated: out.abbreviated,
120                steps: out.steps,
121            }),
122            Err(RenderError::Truncated) => {
123                let budget = limits.bytes.saturating_sub(TRUNCATED.len());
124                let mut end = out.text.len().min(budget);
125                while !out.text.is_char_boundary(end) {
126                    end = end.saturating_sub(1);
127                }
128                out.text.truncate(end);
129                // Very small caller limits still produce a complete, unambiguous marker.
130                out.text
131                    .try_reserve(TRUNCATED.len())
132                    .map_err(|_| RenderError::Allocation)?;
133                out.text.push_str(TRUNCATED);
134                Ok(RenderedText {
135                    text: out.text,
136                    truncated: true,
137                    steps: out.steps,
138                })
139            }
140            Err(error) => Err(error),
141        }
142    }
143
144    pub(crate) fn append(&mut self, rendered: RenderedText) -> Result<(), RenderError> {
145        self.abbreviated |= rendered.truncated;
146        self.steps = self
147            .steps
148            .checked_add(rendered.steps)
149            .ok_or(RenderError::Truncated)?;
150        self.push(&rendered.text)
151    }
152
153    pub(crate) fn child_limits(&self) -> RenderLimits {
154        RenderLimits {
155            bytes: self
156                .limits
157                .bytes
158                .saturating_sub(self.text.len())
159                .max(TRUNCATED.len())
160                .min(RenderLimits::default().bytes),
161            steps: self
162                .limits
163                .steps
164                .saturating_sub(self.steps)
165                .saturating_sub(1),
166            type_depth: self.limits.type_depth,
167        }
168    }
169
170    pub(crate) fn step(&mut self) -> Result<(), RenderError> {
171        if self.steps >= self.limits.steps {
172            return Err(RenderError::Truncated);
173        }
174        self.steps += 1;
175        Ok(())
176    }
177
178    pub(crate) fn depth(&self, depth: usize) -> Result<(), RenderError> {
179        if depth > self.limits.type_depth {
180            Err(RenderError::Truncated)
181        } else {
182            Ok(())
183        }
184    }
185
186    pub(crate) fn push(&mut self, text: &str) -> Result<(), RenderError> {
187        self.step()?;
188        let remaining = self.limits.bytes.saturating_sub(self.text.len());
189        let mut end = remaining.min(text.len());
190        while !text.is_char_boundary(end) {
191            end = end.saturating_sub(1);
192        }
193        let prefix = text.get(..end).ok_or(RenderError::Formatting)?;
194        self.text
195            .try_reserve(prefix.len())
196            .map_err(|_| RenderError::Allocation)?;
197        self.text.push_str(prefix);
198        if end < text.len() {
199            return Err(RenderError::Truncated);
200        }
201        Ok(())
202    }
203
204    pub(crate) fn character(&mut self, ch: char) -> Result<(), RenderError> {
205        self.push(ch.encode_utf8(&mut [0; 4]))
206    }
207
208    pub(crate) fn format(&mut self, args: fmt::Arguments<'_>) -> Result<(), RenderError> {
209        match fmt::write(self, args) {
210            Ok(()) => Ok(()),
211            Err(_) => Err(self.failure.take().unwrap_or(RenderError::Formatting)),
212        }
213    }
214
215    pub(crate) fn repeat(&mut self, ch: char, count: usize) -> Result<(), RenderError> {
216        for _ in 0..count {
217            self.character(ch)?;
218        }
219        Ok(())
220    }
221}
222
223impl fmt::Write for Writer {
224    fn write_str(&mut self, text: &str) -> fmt::Result {
225        self.push(text).map_err(|error| {
226            self.failure = Some(error);
227            fmt::Error
228        })
229    }
230}
231
232/// Source-less compatibility text; never re-enters the failed renderer.
233pub fn failure_text(primary: &str, error: &RenderError) -> String {
234    match Writer::render(RenderLimits::default(), |out| {
235        // Put the reporting failure first so a large primary cannot hide it.
236        out.format(format_args!("internal reporting failure: {error}\n"))?;
237        out.push(primary)
238    }) {
239        Ok(rendered) => rendered.text,
240        Err(_) => "internal reporting failure: diagnostic output unavailable".into(),
241    }
242}
243
244/// Bound a retained diagnostic field before copying it into a response.
245pub fn bounded_text(text: &str, bytes: usize) -> Result<RenderedText, RenderError> {
246    Writer::render(
247        RenderLimits {
248            bytes,
249            ..RenderLimits::default()
250        },
251        |out| out.push(text),
252    )
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn byte_boundaries_are_exact_and_unicode_truncation_is_valid() {
261        for extra in [0, 1] {
262            let input = "é".repeat(32 + extra);
263            let rendered = bounded_text(&input, 64).unwrap();
264            assert!(rendered.text.len() <= 64);
265            assert_eq!(rendered.truncated, extra != 0);
266            if extra == 0 {
267                assert_eq!(rendered.text, input);
268            } else {
269                assert!(rendered.text.ends_with(TRUNCATED));
270            }
271        }
272    }
273
274    #[test]
275    fn work_limit_and_allocation_failure_do_not_publish_partial_success() {
276        let limits = RenderLimits {
277            steps: 2,
278            ..Default::default()
279        };
280        let exact = Writer::render(limits, |out| {
281            out.push("first")?;
282            out.push("second")
283        })
284        .unwrap();
285        assert!(!exact.truncated);
286        let exceeded = Writer::render(limits, |out| {
287            out.push("first")?;
288            out.push("second")?;
289            out.push("third")
290        })
291        .unwrap();
292        assert!(exceeded.truncated);
293        let failed = Writer::render(limits, |out| {
294            out.push("partial")?;
295            Err(RenderError::Allocation)
296        });
297        assert!(matches!(failed, Err(RenderError::Allocation)));
298        assert_eq!(
299            Writer::render(limits, |out| out.push("healthy"))
300                .unwrap()
301                .text,
302            "healthy"
303        );
304    }
305
306    #[test]
307    fn a_broken_display_is_a_typed_failure() {
308        struct Broken;
309        impl fmt::Display for Broken {
310            fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
311                Err(fmt::Error)
312            }
313        }
314        let result = Writer::render(RenderLimits::default(), |out| {
315            out.format(format_args!("{Broken}"))
316        });
317        assert!(matches!(result, Err(RenderError::Formatting)));
318    }
319}