1use std::backtrace::Backtrace;
33use std::fmt;
34
35#[derive(Debug)]
49pub struct IceContext {
50 pub message: String,
52 pub version: Option<String>,
54 pub target: Option<String>,
56 pub phase: Option<String>,
58 pub details: Vec<(String, String)>,
66 pub backtrace: Option<Backtrace>,
68}
69
70impl IceContext {
71 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 pub fn with_version(mut self, version: impl Into<String>) -> Self {
85 self.version = Some(version.into());
86 self
87 }
88
89 pub fn with_target(mut self, target: impl Into<String>) -> Self {
91 self.target = Some(target.into());
92 self
93 }
94
95 pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
97 self.phase = Some(phase.into());
98 self
99 }
100
101 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 pub fn with_backtrace(mut self) -> Self {
114 self.backtrace = Some(Backtrace::capture());
115 self
116 }
117
118 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 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 disabled; set RUST_BACKTRACE=1 to enable)".to_string()
156 } else {
157 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#[macro_export]
223macro_rules! ice {
224 ($msg:expr) => {
226 $crate::ice::IceContext::new($msg)
227 .with_backtrace()
228 };
229
230 ($msg:expr, phase: $phase:expr) => {
232 $crate::ice::IceContext::new($msg)
233 .with_phase($phase)
234 .with_backtrace()
235 };
236
237 ($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 ($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#[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 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 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 #[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}