gruel_error/
ice.rs

1//! Internal Compiler Error (ICE) handling.
2//!
3//! This module provides rich context capture for internal compiler errors to
4//! improve bug reports and developer debugging experience.
5//!
6//! # Creating ICEs
7//!
8//! Use the [`ice!`] macro for easy ICE creation:
9//!
10//! ```ignore
11//! use gruel_error::ice;
12//!
13//! // Simple ICE with just a message
14//! let ctx = ice!("unexpected type in codegen");
15//!
16//! // ICE with phase information
17//! let ctx = ice!("invalid instruction", phase: "codegen/emit");
18//!
19//! // ICE with custom details
20//! let ctx = ice!("type mismatch",
21//!     phase: "sema",
22//!     details: {
23//!         "expected" => "i32",
24//!         "found" => "bool"
25//!     }
26//! );
27//!
28//! // Create a CompileError directly
29//! return Err(ice_error!("codegen failed", phase: "emit"));
30//! ```
31
32use std::backtrace::Backtrace;
33use std::fmt;
34
35/// Context information for an Internal Compiler Error (ICE).
36///
37/// This struct captures detailed information about the compiler state when an
38/// ICE occurs, making it easier to diagnose and fix compiler bugs.
39///
40/// # Example
41/// ```ignore
42/// let ice = IceContext::new("unexpected type in codegen")
43///     .with_version("0.1.0")
44///     .with_target("x86_64-unknown-linux-gnu")
45///     .with_phase("codegen/emit")
46///     .with_backtrace();
47/// ```
48#[derive(Debug)]
49pub struct IceContext {
50    /// The error message describing what went wrong.
51    pub message: String,
52    /// Compiler version (from CARGO_PKG_VERSION).
53    pub version: Option<String>,
54    /// Target architecture (e.g., "x86_64-unknown-linux-gnu").
55    pub target: Option<String>,
56    /// Compilation phase (e.g., "codegen/emit", "sema", "cfg_builder").
57    pub phase: Option<String>,
58    /// Additional context-specific details.
59    ///
60    /// This can include things like:
61    /// - Current function being compiled
62    /// - Instruction that triggered the ICE
63    /// - Type information
64    /// - Any other relevant state
65    pub details: Vec<(String, String)>,
66    /// Backtrace captured at the ICE site.
67    pub backtrace: Option<Backtrace>,
68}
69
70impl IceContext {
71    /// Create a new ICE context with the given error message.
72    pub fn new(message: impl Into<String>) -> Self {
73        Self {
74            message: message.into(),
75            version: None,
76            target: None,
77            phase: None,
78            details: Vec::new(),
79            backtrace: None,
80        }
81    }
82
83    /// Set the compiler version.
84    pub fn with_version(mut self, version: impl Into<String>) -> Self {
85        self.version = Some(version.into());
86        self
87    }
88
89    /// Set the target architecture.
90    pub fn with_target(mut self, target: impl Into<String>) -> Self {
91        self.target = Some(target.into());
92        self
93    }
94
95    /// Set the compilation phase.
96    pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
97        self.phase = Some(phase.into());
98        self
99    }
100
101    /// Add a detail key-value pair.
102    ///
103    /// Details provide context-specific information about the compiler state.
104    pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
105        self.details.push((key.into(), value.into()));
106        self
107    }
108
109    /// Capture a backtrace at the current call site.
110    ///
111    /// This should be called at the point where the ICE is detected to capture
112    /// the most relevant stack trace.
113    pub fn with_backtrace(mut self) -> Self {
114        self.backtrace = Some(Backtrace::capture());
115        self
116    }
117
118    /// Format the ICE context for display.
119    ///
120    /// This produces a user-friendly representation suitable for error messages.
121    pub fn format_details(&self) -> String {
122        let mut output = String::new();
123
124        if let Some(version) = &self.version {
125            output.push_str(&format!("  gruel version: {}\n", version));
126        }
127
128        if let Some(target) = &self.target {
129            output.push_str(&format!("  target: {}\n", target));
130        }
131
132        if let Some(phase) = &self.phase {
133            output.push_str(&format!("  phase: {}\n", phase));
134        }
135
136        if !self.details.is_empty() {
137            output.push_str("\n  relevant state:\n");
138            for (key, value) in &self.details {
139                output.push_str(&format!("    {}: {}\n", key, value));
140            }
141        }
142
143        output
144    }
145
146    /// Format the backtrace for display.
147    ///
148    /// Returns a formatted backtrace if one was captured, or None otherwise.
149    /// The backtrace is formatted with frame numbers and source locations.
150    pub fn format_backtrace(&self) -> Option<String> {
151        self.backtrace.as_ref().map(|bt| {
152            let bt_str = format!("{}", bt);
153            if bt_str.trim().is_empty() || bt_str.contains("disabled") {
154                // Backtrace capture is disabled
155                "  (backtrace capture disabled; set RUST_BACKTRACE=1 to enable)".to_string()
156            } else {
157                // Format each frame with indentation
158                bt_str
159                    .lines()
160                    .map(|line| format!("  {}", line))
161                    .collect::<Vec<_>>()
162                    .join("\n")
163            }
164        })
165    }
166}
167
168impl fmt::Display for IceContext {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        write!(f, "internal compiler error: {}", self.message)?;
171
172        let has_details = self.version.is_some()
173            || self.target.is_some()
174            || self.phase.is_some()
175            || !self.details.is_empty();
176
177        if has_details {
178            write!(f, "\n\ndebug info:\n{}", self.format_details())?;
179        }
180
181        if let Some(backtrace) = self.format_backtrace() {
182            if has_details {
183                writeln!(f)?;
184            } else {
185                write!(f, "\n\n")?;
186            }
187            write!(f, "backtrace:\n{}", backtrace)?;
188        }
189
190        Ok(())
191    }
192}
193
194/// Create an [`IceContext`] with automatic version and backtrace capture.
195///
196/// This macro provides a convenient way to create ICE contexts with common
197/// defaults while allowing customization of phase and details.
198///
199/// # Syntax
200///
201/// ```ignore
202/// // Just a message
203/// ice!("error message")
204///
205/// // Message + phase
206/// ice!("error message", phase: "codegen/emit")
207///
208/// // Message + details
209/// ice!("error message", details: { "key1" => "value1", "key2" => "value2" })
210///
211/// // Message + phase + details
212/// ice!("error message",
213///     phase: "sema",
214///     details: { "expected" => "i32", "found" => "bool" }
215/// )
216/// ```
217///
218/// The macro automatically:
219/// - Captures a backtrace
220///
221/// Callers should add version information using `.with_version()` when creating ICEs.
222#[macro_export]
223macro_rules! ice {
224    // Just message
225    ($msg:expr) => {
226        $crate::ice::IceContext::new($msg)
227            .with_backtrace()
228    };
229
230    // Message + phase
231    ($msg:expr, phase: $phase:expr) => {
232        $crate::ice::IceContext::new($msg)
233            .with_phase($phase)
234            .with_backtrace()
235    };
236
237    // Message + details
238    ($msg:expr, details: { $($key:expr => $value:expr),+ $(,)? }) => {{
239        let mut ctx = $crate::ice::IceContext::new($msg)
240            .with_backtrace();
241        $(
242            ctx = ctx.with_detail($key, $value);
243        )+
244        ctx
245    }};
246
247    // Message + phase + details
248    ($msg:expr, phase: $phase:expr, details: { $($key:expr => $value:expr),+ $(,)? }) => {{
249        let mut ctx = $crate::ice::IceContext::new($msg)
250            .with_phase($phase)
251            .with_backtrace();
252        $(
253            ctx = ctx.with_detail($key, $value);
254        )+
255        ctx
256    }};
257}
258
259/// Create a [`CompileError`] from an ICE context.
260///
261/// This is a convenience wrapper around [`ice!`] that wraps the result
262/// in a [`CompileError`] for direct use in error returns.
263///
264/// # Syntax
265///
266/// Same as [`ice!`], but returns a [`CompileError`]:
267///
268/// ```ignore
269/// return Err(ice_error!("unexpected type"));
270/// return Err(ice_error!("invalid instruction", phase: "codegen"));
271/// ```
272///
273/// [`CompileError`]: crate::CompileError
274#[macro_export]
275macro_rules! ice_error {
276    ($($tt:tt)*) => {
277        $crate::CompileError::without_span(
278            $crate::ErrorKind::InternalError($crate::ice!($($tt)*).to_string())
279        )
280    };
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_ice_context_new() {
289        let ice = IceContext::new("test error");
290        assert_eq!(ice.message, "test error");
291        assert!(ice.version.is_none());
292        assert!(ice.target.is_none());
293        assert!(ice.phase.is_none());
294        assert!(ice.details.is_empty());
295    }
296
297    #[test]
298    fn test_ice_context_with_version() {
299        let ice = IceContext::new("test error").with_version("0.1.0");
300        assert_eq!(ice.version.as_deref(), Some("0.1.0"));
301    }
302
303    #[test]
304    fn test_ice_context_with_target() {
305        let ice = IceContext::new("test error").with_target("x86_64-unknown-linux-gnu");
306        assert_eq!(ice.target.as_deref(), Some("x86_64-unknown-linux-gnu"));
307    }
308
309    #[test]
310    fn test_ice_context_with_phase() {
311        let ice = IceContext::new("test error").with_phase("codegen/emit");
312        assert_eq!(ice.phase.as_deref(), Some("codegen/emit"));
313    }
314
315    #[test]
316    fn test_ice_context_with_detail() {
317        let ice = IceContext::new("test error")
318            .with_detail("current_function", "main")
319            .with_detail("instruction", "Call");
320        assert_eq!(ice.details.len(), 2);
321        assert_eq!(
322            ice.details[0],
323            ("current_function".to_string(), "main".to_string())
324        );
325        assert_eq!(
326            ice.details[1],
327            ("instruction".to_string(), "Call".to_string())
328        );
329    }
330
331    #[test]
332    fn test_ice_context_builder_chain() {
333        let ice = IceContext::new("unexpected type")
334            .with_version("0.1.0")
335            .with_target("x86_64-unknown-linux-gnu")
336            .with_phase("codegen/emit")
337            .with_detail("function", "main")
338            .with_detail("instruction", "Call");
339
340        assert_eq!(ice.message, "unexpected type");
341        assert_eq!(ice.version.as_deref(), Some("0.1.0"));
342        assert_eq!(ice.target.as_deref(), Some("x86_64-unknown-linux-gnu"));
343        assert_eq!(ice.phase.as_deref(), Some("codegen/emit"));
344        assert_eq!(ice.details.len(), 2);
345    }
346
347    #[test]
348    fn test_ice_context_format_details_minimal() {
349        let ice = IceContext::new("test error");
350        let formatted = ice.format_details();
351        assert_eq!(formatted, "");
352    }
353
354    #[test]
355    fn test_ice_context_format_details_with_version() {
356        let ice = IceContext::new("test error").with_version("0.1.0");
357        let formatted = ice.format_details();
358        assert!(formatted.contains("gruel version: 0.1.0"));
359    }
360
361    #[test]
362    fn test_ice_context_format_details_full() {
363        let ice = IceContext::new("test error")
364            .with_version("0.1.0")
365            .with_target("x86_64-unknown-linux-gnu")
366            .with_phase("codegen")
367            .with_detail("function", "main");
368
369        let formatted = ice.format_details();
370        assert!(formatted.contains("gruel version: 0.1.0"));
371        assert!(formatted.contains("target: x86_64-unknown-linux-gnu"));
372        assert!(formatted.contains("phase: codegen"));
373        assert!(formatted.contains("relevant state:"));
374        assert!(formatted.contains("function: main"));
375    }
376
377    #[test]
378    fn test_ice_context_display_minimal() {
379        let ice = IceContext::new("test error");
380        assert_eq!(ice.to_string(), "internal compiler error: test error");
381    }
382
383    #[test]
384    fn test_ice_context_display_with_details() {
385        let ice = IceContext::new("test error")
386            .with_version("0.1.0")
387            .with_phase("codegen");
388
389        let output = ice.to_string();
390        assert!(output.contains("internal compiler error: test error"));
391        assert!(output.contains("debug info:"));
392        assert!(output.contains("gruel version: 0.1.0"));
393        assert!(output.contains("phase: codegen"));
394    }
395
396    #[test]
397    fn test_ice_context_with_backtrace() {
398        let ice = IceContext::new("test error").with_backtrace();
399        assert!(ice.backtrace.is_some());
400    }
401
402    #[test]
403    fn test_ice_context_format_backtrace_when_none() {
404        let ice = IceContext::new("test error");
405        assert!(ice.format_backtrace().is_none());
406    }
407
408    #[test]
409    fn test_ice_context_format_backtrace_when_captured() {
410        let ice = IceContext::new("test error").with_backtrace();
411        let formatted = ice.format_backtrace();
412        assert!(formatted.is_some());
413        // The backtrace should either contain actual frames or the disabled message
414        let bt_str = formatted.unwrap();
415        assert!(bt_str.contains("backtrace capture disabled") || !bt_str.is_empty());
416    }
417
418    #[test]
419    fn test_ice_context_display_with_backtrace() {
420        let ice = IceContext::new("test error")
421            .with_version("0.1.0")
422            .with_backtrace();
423
424        let output = ice.to_string();
425        assert!(output.contains("internal compiler error: test error"));
426        assert!(output.contains("backtrace:"));
427    }
428
429    #[test]
430    fn test_ice_context_full_builder() {
431        // Test the full builder chain with backtrace
432        let ice = IceContext::new("unexpected type")
433            .with_version("0.1.0")
434            .with_target("x86_64-unknown-linux-gnu")
435            .with_phase("codegen/emit")
436            .with_detail("function", "main")
437            .with_backtrace();
438
439        assert_eq!(ice.message, "unexpected type");
440        assert!(ice.version.is_some());
441        assert!(ice.target.is_some());
442        assert!(ice.phase.is_some());
443        assert_eq!(ice.details.len(), 1);
444        assert!(ice.backtrace.is_some());
445    }
446
447    // ========================================================================
448    // Macro tests
449    // ========================================================================
450
451    #[test]
452    fn test_ice_macro_simple() {
453        let ctx = ice!("test error");
454        assert_eq!(ctx.message, "test error");
455        assert!(ctx.backtrace.is_some());
456        assert!(ctx.phase.is_none());
457        assert!(ctx.details.is_empty());
458    }
459
460    #[test]
461    fn test_ice_macro_with_phase() {
462        let ctx = ice!("test error", phase: "codegen");
463        assert_eq!(ctx.message, "test error");
464        assert_eq!(ctx.phase.as_deref(), Some("codegen"));
465        assert!(ctx.backtrace.is_some());
466    }
467
468    #[test]
469    fn test_ice_macro_with_details() {
470        let ctx = ice!("test error", details: {
471            "key1" => "value1",
472            "key2" => "value2"
473        });
474        assert_eq!(ctx.message, "test error");
475        assert_eq!(ctx.details.len(), 2);
476        assert_eq!(ctx.details[0], ("key1".to_string(), "value1".to_string()));
477        assert_eq!(ctx.details[1], ("key2".to_string(), "value2".to_string()));
478    }
479
480    #[test]
481    fn test_ice_macro_with_phase_and_details() {
482        let ctx = ice!("test error",
483            phase: "sema",
484            details: {
485                "expected" => "i32",
486                "found" => "bool"
487            }
488        );
489        assert_eq!(ctx.message, "test error");
490        assert_eq!(ctx.phase.as_deref(), Some("sema"));
491        assert_eq!(ctx.details.len(), 2);
492    }
493
494    #[test]
495    fn test_ice_error_macro_simple() {
496        let err = ice_error!("test error");
497        let output = err.to_string();
498        assert!(output.contains("test error"));
499    }
500
501    #[test]
502    fn test_ice_error_macro_with_phase() {
503        let err = ice_error!("test error", phase: "codegen");
504        let output = err.to_string();
505        assert!(output.contains("test error"));
506        assert!(output.contains("codegen"));
507    }
508
509    #[test]
510    fn test_ice_error_returns_compile_error() {
511        fn make_error() -> Result<(), crate::CompileError> {
512            Err(ice_error!("test"))
513        }
514        assert!(make_error().is_err());
515    }
516}