Skip to main content

geam_core/runtime/error/
diagnostic.rs

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}