1use super::{
2 BitArraySegmentPanicReason, ExecutionError, HostError, InvariantError, Panic, PanicDetails,
3};
4use crate::plan::{FunctionType, ValueType};
5use crate::runtime::Value;
6use miette::{Diagnostic, LabeledSpan, SourceCode};
7use std::fmt;
8
9impl Diagnostic for ExecutionError {
10 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
11 match self {
12 Self::Panic(panic) => panic.code(),
13 Self::Invariant(error) => error.code(),
14 Self::Host(error) => error.code(),
15 }
16 }
17
18 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
19 match self {
20 Self::Panic(panic) => panic.help(),
21 Self::Invariant(error) => error.help(),
22 Self::Host(error) => error.help(),
23 }
24 }
25
26 fn source_code(&self) -> Option<&dyn SourceCode> {
27 match self {
28 Self::Panic(panic) => panic.source_code(),
29 Self::Invariant(error) => error.source_code(),
30 Self::Host(error) => error.source_code(),
31 }
32 }
33
34 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
35 match self {
36 Self::Panic(panic) => panic.labels(),
37 Self::Invariant(error) => error.labels(),
38 Self::Host(error) => error.labels(),
39 }
40 }
41}
42
43impl Diagnostic for Panic {
44 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
45 Some(Box::new(format!("geam::{}", self.kind().code())))
46 }
47
48 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
49 match self.details() {
50 Some(PanicDetails::LetAssert { value, .. }) => {
51 Some(Box::new(format!("failed value: {}", render_value(value))))
52 }
53 Some(PanicDetails::BitArraySegment { reason }) => Some(Box::new(match reason {
54 BitArraySegmentPanicReason::InvalidFloatSize { bit_size } => format!(
55 "float segments must be 16, 32, or 64 bits; evaluated size was {bit_size} bits"
56 ),
57 BitArraySegmentPanicReason::InsufficientBits {
58 requested,
59 available,
60 } => format!(
61 "sized bits segment requested {requested} bits, but the value contains {available} bits"
62 ),
63 BitArraySegmentPanicReason::SizeOutOfRange { bit_size } => {
64 format!("BitArray segment size {bit_size} exceeds the supported host range")
65 }
66 })),
67 None => None,
68 }
69 }
70
71 fn source_code(&self) -> Option<&dyn SourceCode> {
72 self.source().map(|source| source as &dyn SourceCode)
73 }
74
75 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
76 self.source()?;
77
78 let mut labels = vec![LabeledSpan::new_primary_with_span(
79 Some(self.primary_label()),
80 self.site().span().to_miette(),
81 )];
82 if let Some(PanicDetails::LetAssert { pattern_span, .. }) = self.details() {
83 labels.push(LabeledSpan::at(pattern_span.to_miette(), "pattern"));
84 }
85
86 Some(Box::new(labels.into_iter()))
87 }
88}
89
90impl Diagnostic for HostError {
91 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
92 Some(Box::new("geam::host_function"))
93 }
94
95 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
96 None
97 }
98
99 fn source_code(&self) -> Option<&dyn SourceCode> {
100 self.source().map(|source| source as &dyn SourceCode)
101 }
102
103 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
104 let super::HostLocation::Resolved { site, .. } = self.location() else {
105 return None;
106 };
107 Some(Box::new(std::iter::once(
108 LabeledSpan::new_primary_with_span(Some(self.primary_label()), site.span().to_miette()),
109 )))
110 }
111}
112
113impl Diagnostic for InvariantError {
114 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
115 match self {
116 Self::FunctionReturnFamilyMismatch { .. } => {
117 Some(Box::new("geam::function_return_family_mismatch"))
118 }
119 Self::TupleIndexFamilyMismatch { .. } => Some(Box::new("geam::tuple_index_mismatch")),
120 Self::CustomFieldFamilyMismatch { .. } => {
121 Some(Box::new("geam::custom_field_family_mismatch"))
122 }
123 Self::ListIndexOutOfBounds { .. } => Some(Box::new("geam::list_index_out_of_bounds")),
124 }
125 }
126
127 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
128 None
129 }
130
131 fn source_code(&self) -> Option<&dyn SourceCode> {
132 None
133 }
134
135 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
136 None
137 }
138}
139
140fn render_value(value: &Value) -> String {
141 match value {
142 Value::Int(value) => format!("Int({value})"),
143 Value::Float(value) => format!("Float({value:?})"),
144 Value::String(value) => format!("String({value:?})"),
145 Value::BitArray(value) => format!(
146 "BitArray(bytes={:?}, bit_len={})",
147 value.bytes(),
148 value.bit_len(),
149 ),
150 Value::UtfCodepoint(value) => format!("UtfCodepoint({value:?})"),
151 Value::Custom(value) => format!(
152 "{}::{}({})",
153 render_custom_type(value.type_()),
154 value.constructor_name(),
155 value
156 .fields()
157 .iter()
158 .map(|field| render_value(field.value()))
159 .collect::<Vec<_>>()
160 .join(", ")
161 ),
162 Value::External(value) => format!("External({})", value.inspection()),
163 Value::Bool(value) => format!("Bool({value})"),
164 Value::Nil => "Nil".into(),
165 Value::Tuple(values) => format!(
166 "Tuple([{}])",
167 values
168 .iter()
169 .map(render_value)
170 .collect::<Vec<_>>()
171 .join(", ")
172 ),
173 Value::List(value) => format!(
174 "List({})([{}])",
175 render_value_type(&value.item_type()),
176 value
177 .to_values()
178 .iter()
179 .map(render_value)
180 .collect::<Vec<_>>()
181 .join(", ")
182 ),
183 Value::Function(function) => {
184 let type_ = function.type_();
185 format!("Function({})", render_function_type(&type_))
186 }
187 }
188}
189
190fn render_function_type(type_: &FunctionType) -> String {
191 let arguments = type_
192 .argument_types()
193 .iter()
194 .map(render_value_type)
195 .collect::<Vec<_>>()
196 .join(", ");
197
198 format!("fn({arguments}) -> {}", render_value_type(type_.return_()))
199}
200
201fn render_value_type(type_: &ValueType) -> String {
202 match type_ {
203 ValueType::Parameter(parameter) => format!("Parameter({})", parameter.index()),
204 ValueType::Int => "Int".into(),
205 ValueType::Float => "Float".into(),
206 ValueType::String => "String".into(),
207 ValueType::BitArray => "BitArray".into(),
208 ValueType::UtfCodepoint => "UtfCodepoint".into(),
209 ValueType::Bool => "Bool".into(),
210 ValueType::Nil => "Nil".into(),
211 ValueType::Tuple(types) => format!(
212 "#({})",
213 types
214 .iter()
215 .map(render_value_type)
216 .collect::<Vec<_>>()
217 .join(", ")
218 ),
219 ValueType::List(element) => format!("List({})", render_value_type(element)),
220 ValueType::Function(type_) => render_function_type(type_),
221 ValueType::Custom(type_) => render_custom_type(type_),
222 ValueType::External(type_) => render_external_type(type_),
223 }
224}
225
226fn render_custom_type(type_: &crate::plan::CustomType) -> String {
227 let name = type_.type_name();
228 let identity = format!("{}/{}/{}", name.package(), name.module(), name.name());
229 if type_.arguments().is_empty() {
230 identity
231 } else {
232 format!(
233 "{}({})",
234 identity,
235 type_
236 .arguments()
237 .iter()
238 .map(render_value_type)
239 .collect::<Vec<_>>()
240 .join(", ")
241 )
242 }
243}
244
245fn render_external_type(type_: &crate::plan::ExternalType) -> String {
246 let name = type_.type_name();
247 let identity = format!("{}::{}.{}", name.package(), name.module(), name.name());
248 if type_.arguments().is_empty() {
249 identity
250 } else {
251 format!(
252 "{identity}({})",
253 type_
254 .arguments()
255 .iter()
256 .map(render_value_type)
257 .collect::<Vec<_>>()
258 .join(", "),
259 )
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::{render_value, render_value_type};
266 use crate::host::{HostExternalStore, HostFailure};
267 use crate::plan::execution::function::{
268 CoreRuntimeFunctionId, FunctionReturnFamily, IntFunctionId, RuntimeFunctionId,
269 };
270 use crate::plan::execution::graph::{IntLocalId, ParamLocal};
271 use crate::plan::{
272 CustomType, CustomTypeName, ExternalType, ExternalTypeName, FunctionType, HostCallSite,
273 PanicSite, SourceContext, SourceSpan, ValueType,
274 };
275 use crate::runtime::{BitArrayValue, ExternalValue, FunctionValue, ListValue, Value};
276 use crate::runtime::{
277 ExecutionError, HostError, InvariantError, Panic, PanicDetails, PanicKind, PanicMessage,
278 };
279 use miette::Diagnostic;
280
281 #[test]
282 fn source_less_panic_diagnostic_has_no_source_labels_or_help() {
283 let error =
284 ExecutionError::source_panic(None, PanicKind::Panic, None, PanicSite::unknown());
285
286 assert_eq!(
287 error.code().map(|code| code.to_string()),
288 Some("geam::panic".into()),
289 );
290 assert!(error.help().is_none());
291 assert!(error.source_code().is_none());
292 assert!(error.labels().is_none());
293 }
294
295 #[test]
296 fn host_diagnostic_and_execution_wrapper_preserve_source_labels() {
297 let source = SourceContext::new("src/main.gleam", "pub fn main() {\n fail(1)\n}");
298 let host = HostError::new(
299 "host_support".into(),
300 "host/service".into(),
301 "fail".into(),
302 FunctionType::new(vec![ValueType::Int], ValueType::Int),
303 HostFailure::new("unavailable"),
304 HostCallSite::new("main".into(), "main".into(), SourceSpan::new(18, 25)),
305 Some(&source),
306 );
307 let labels = host
308 .labels()
309 .expect("source-backed host failure should have a label")
310 .collect::<Vec<_>>();
311
312 assert_eq!(
313 host.code().map(|code| code.to_string()),
314 Some("geam::host_function".into()),
315 );
316 assert!(host.help().is_none());
317 assert!(host.source_code().is_some());
318 assert_eq!(labels.len(), 1);
319 assert_eq!(
320 labels[0].label(),
321 Some("host function host_support::host/service.fail failed"),
322 );
323 assert_eq!(labels[0].offset(), 18);
324 assert_eq!(labels[0].len(), 7);
325
326 let error = ExecutionError::Host(Box::new(host));
327 assert_eq!(
328 error.code().map(|code| code.to_string()),
329 Some("geam::host_function".into()),
330 );
331 assert!(error.help().is_none());
332 assert!(error.source_code().is_some());
333 assert_eq!(
334 error
335 .labels()
336 .expect("execution wrapper should retain the host label")
337 .count(),
338 1,
339 );
340 }
341
342 #[test]
343 fn source_less_host_diagnostic_has_no_source_or_labels() {
344 let host = HostError::new(
345 "host_support".into(),
346 "host/service".into(),
347 "fail".into(),
348 FunctionType::new(Vec::new(), ValueType::Bool),
349 HostFailure::new("unavailable"),
350 HostCallSite::new("host/service".into(), "fail".into(), SourceSpan::new(0, 0)),
351 None,
352 );
353
354 assert!(host.source_code().is_none());
355 assert!(host.labels().is_none());
356 }
357
358 #[test]
359 fn panic_diagnostic_has_source_labels_and_failed_value_help() {
360 let source = SourceContext::new(
361 "main.gleam",
362 "pub fn main() {\n let assert [x, ..] = []\n}",
363 );
364 let panic = Panic::new(
365 PanicKind::LetAssert,
366 PanicMessage::Default,
367 PanicSite::new("main".into(), "main".into(), SourceSpan::new(18, 43)),
368 Some(&source),
369 Some(PanicDetails::LetAssert {
370 value: Value::List(crate::runtime::ListValue::empty(ValueType::Int)),
371 pattern_span: SourceSpan::new(29, 36),
372 }),
373 );
374 let labels = panic
375 .labels()
376 .expect("source-backed panic should have labels")
377 .collect::<Vec<_>>();
378
379 assert_eq!(
380 panic.code().map(|code| code.to_string()),
381 Some("geam::let_assert".into())
382 );
383 assert!(panic.source_code().is_some());
384 assert_eq!(labels.len(), 2);
385 assert_eq!(labels[0].label(), Some("let assert in main.main"));
386 assert_eq!(labels[0].offset(), 18);
387 assert_eq!(labels[0].len(), 25);
388 assert_eq!(labels[1].label(), Some("pattern"));
389 assert_eq!(labels[1].offset(), 29);
390 assert_eq!(
391 panic.help().map(|help| help.to_string()),
392 Some("failed value: List(Int)([])".into()),
393 );
394 }
395
396 #[test]
397 fn source_backed_panic_without_details_has_one_primary_label() {
398 let source = SourceContext::new("main.gleam", "pub fn main() {\n assert False\n}");
399 let panic = Panic::new(
400 PanicKind::Assert,
401 PanicMessage::Default,
402 PanicSite::new("main".into(), "main".into(), SourceSpan::new(18, 30)),
403 Some(&source),
404 None,
405 );
406 let labels = panic
407 .labels()
408 .expect("source-backed panic should have labels")
409 .collect::<Vec<_>>();
410
411 assert_eq!(labels.len(), 1);
412 assert_eq!(labels[0].label(), Some("assert in main.main"));
413 assert_eq!(labels[0].offset(), 18);
414 assert_eq!(labels[0].len(), 12);
415 assert!(panic.help().is_none());
416 }
417
418 #[test]
419 fn invariant_diagnostics_have_codes_without_source_labels_or_help() {
420 for (invariant, expected_code) in [
421 (
422 InvariantError::FunctionReturnFamilyMismatch {
423 expected: FunctionReturnFamily::Int,
424 actual: FunctionReturnFamily::String,
425 },
426 "geam::function_return_family_mismatch",
427 ),
428 (
429 InvariantError::TupleIndexFamilyMismatch {
430 expected: ValueType::Int,
431 actual: ValueType::String,
432 },
433 "geam::tuple_index_mismatch",
434 ),
435 (
436 InvariantError::CustomFieldFamilyMismatch {
437 custom_type: CustomType::new(
438 CustomTypeName::new("geam".into(), "main".into(), "Boxed".into()),
439 Vec::new(),
440 ),
441 constructor: "Boxed".into(),
442 field_index: 0,
443 expected: ValueType::Int,
444 actual: ValueType::String,
445 },
446 "geam::custom_field_family_mismatch",
447 ),
448 (
449 InvariantError::ListIndexOutOfBounds {
450 item_type: ValueType::Int,
451 index: 1,
452 length: 1,
453 },
454 "geam::list_index_out_of_bounds",
455 ),
456 ] {
457 assert_eq!(
458 invariant.code().map(|code| code.to_string()),
459 Some(expected_code.into()),
460 );
461 assert!(invariant.help().is_none());
462 assert!(invariant.source_code().is_none());
463 assert!(invariant.labels().is_none());
464
465 let error = ExecutionError::Invariant(invariant);
466
467 assert_eq!(
468 error.code().map(|code| code.to_string()),
469 Some(expected_code.into()),
470 );
471 assert!(error.help().is_none());
472 assert!(error.source_code().is_none());
473 assert!(error.labels().is_none());
474 }
475 }
476
477 #[test]
478 fn render_value_preserves_every_runtime_value_family() {
479 fn source_hash(
480 context: &crate::host::HostExternalHashing<'_>,
481 value: &crate::host::HostStoredValue<num_bigint::BigInt>,
482 ) -> u64 {
483 context.stored_value_hash(value)
484 }
485
486 fn inspect(
487 context: &crate::host::HostExternalInspection<'_>,
488 value: &crate::host::HostStoredValue<num_bigint::BigInt>,
489 ) -> ecow::EcoString {
490 context.inspect_stored_value(value)
491 }
492
493 let external_store = HostExternalStore::default();
494 let source_equal =
495 |context: &crate::host::HostExternalEquality<'_>,
496 left: &crate::host::HostStoredValue<num_bigint::BigInt>,
497 right: &crate::host::HostStoredValue<num_bigint::BigInt>| {
498 context.stored_values_equal(left, right)
499 };
500 let first = external_store.insert(
501 crate::host::HostStoredValue::new(crate::runtime::StoredRuntimeValue::test_int(
502 7.into(),
503 )),
504 source_equal,
505 source_hash,
506 inspect,
507 );
508 let equal = external_store.insert(
509 crate::host::HostStoredValue::new(crate::runtime::StoredRuntimeValue::test_int(
510 7.into(),
511 )),
512 source_equal,
513 source_hash,
514 inspect,
515 );
516 let stored_equal =
517 |left: &crate::runtime::StoredRuntimeValue,
518 right: &crate::runtime::StoredRuntimeValue| left.value() == right.value();
519 let equality = crate::host::HostExternalEquality::new(&stored_equal);
520 assert!(first.source_equal(&equality, &equal));
521 let stored_hash = |_: &crate::runtime::StoredRuntimeValue| 7;
522 let stored_inspect = |_: &crate::runtime::StoredRuntimeValue| "Resource(7)".into();
523 assert_eq!(
524 first.source_hash(&crate::host::HostExternalHashing::new(&stored_hash)),
525 7,
526 );
527 assert_eq!(
528 first.inspection(&crate::host::HostExternalInspection::new(&stored_inspect)),
529 "Resource(7)",
530 );
531 let external = ExternalValue::from_evaluated(
532 ExternalType::new(
533 ExternalTypeName::new("domain".into(), "domain/resource".into(), "Resource".into()),
534 Vec::new(),
535 ),
536 first,
537 "Resource(7)".into(),
538 );
539 let function = Value::Function(FunctionValue::new(
540 RuntimeFunctionId::Core(CoreRuntimeFunctionId::Int(IntFunctionId(0))),
541 vec![ParamLocal::Int(IntLocalId(0))],
542 crate::plan::FunctionType::new(
543 vec![crate::plan::ValueType::Int],
544 crate::plan::ValueType::Int,
545 ),
546 ));
547
548 for (value, expected) in [
549 (Value::Int(1.into()), "Int(1)"),
550 (Value::Float(1.5), "Float(1.5)"),
551 (Value::String("one".into()), "String(\"one\")"),
552 (
553 Value::BitArray(BitArrayValue::from_bytes(vec![0xa5])),
554 "BitArray(bytes=[165], bit_len=8)",
555 ),
556 (
557 Value::UtfCodepoint('\u{10ffff}'),
558 "UtfCodepoint('\\u{10ffff}')",
559 ),
560 (Value::External(external), "External(Resource(7))"),
561 (Value::Bool(true), "Bool(true)"),
562 (Value::Nil, "Nil"),
563 (
564 Value::Tuple(vec![Value::Int(1.into()), Value::String("one".into())]),
565 "Tuple([Int(1), String(\"one\")])",
566 ),
567 (
568 Value::List(ListValue::int(vec![1.into(), 2.into()])),
569 "List(Int)([Int(1), Int(2)])",
570 ),
571 (function, "Function(fn(Int) -> Int)"),
572 ] {
573 assert_eq!(render_value(&value), expected);
574 }
575
576 let custom =
577 crate::runtime::run_src("pub type Boxed { Boxed(Int) } pub fn main() { Boxed(1) }");
578 assert_eq!(render_value(&custom), "geam/main/Boxed::Boxed(Int(1))",);
579 }
580
581 #[test]
582 fn render_value_type_preserves_compound_shapes() {
583 assert_eq!(
584 render_value_type(&ValueType::Parameter(crate::plan::TypeParameterId(2))),
585 "Parameter(2)",
586 );
587 assert_eq!(render_value_type(&ValueType::BitArray), "BitArray");
588 assert_eq!(render_value_type(&ValueType::UtfCodepoint), "UtfCodepoint");
589 assert_eq!(
590 render_value_type(&ValueType::Tuple(vec![ValueType::Int, ValueType::String])),
591 "#(Int, String)",
592 );
593 assert_eq!(
594 render_value_type(&ValueType::List(Box::new(ValueType::Bool))),
595 "List(Bool)",
596 );
597 assert_eq!(
598 render_value_type(&ValueType::Function(Box::new(FunctionType::new(
599 vec![ValueType::Float],
600 ValueType::Nil,
601 )))),
602 "fn(Float) -> Nil",
603 );
604 assert_eq!(
605 render_value_type(&ValueType::Custom(CustomType::new(
606 CustomTypeName::new("geam".into(), "main".into(), "Boxed".into()),
607 vec![ValueType::Int],
608 ))),
609 "geam/main/Boxed(Int)",
610 );
611 assert_eq!(
612 render_value_type(&ValueType::External(ExternalType::new(
613 ExternalTypeName::new("domain".into(), "domain/resource".into(), "Resource".into(),),
614 vec![ValueType::String],
615 ))),
616 "domain::domain/resource.Resource(String)",
617 );
618 assert_eq!(
619 render_value_type(&ValueType::External(ExternalType::new(
620 ExternalTypeName::new("domain".into(), "domain/resource".into(), "Resource".into(),),
621 Vec::new(),
622 ))),
623 "domain::domain/resource.Resource",
624 );
625 }
626}