Skip to main content

yulang_runtime/
diagnostic.rs

1use std::fmt;
2
3use yulang_typed_ir as typed_ir;
4
5pub type RuntimeResult<T> = Result<T, RuntimeError>;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum RuntimeError {
9    MissingBindingType {
10        path: typed_ir::Path,
11    },
12    MissingRootType {
13        index: usize,
14    },
15    MissingLocalType {
16        path: typed_ir::Path,
17    },
18    MissingExpectedType {
19        node: &'static str,
20    },
21    MissingApplyEvidence,
22    MissingJoinEvidence {
23        node: &'static str,
24    },
25    NonFunctionCallee {
26        ty: typed_ir::Type,
27    },
28    ExpectedThunk {
29        ty: typed_ir::Type,
30    },
31    TypeMismatch {
32        expected: typed_ir::Type,
33        actual: typed_ir::Type,
34        source: TypeSource,
35        context: Option<TypeMismatchContext>,
36    },
37    UnsupportedPatternShape {
38        pattern: &'static str,
39        ty: typed_ir::Type,
40    },
41    UnsupportedSelectBase {
42        field: typed_ir::Name,
43        ty: typed_ir::Type,
44    },
45    UnboundVariable {
46        path: typed_ir::Path,
47    },
48    ResidualAny {
49        ty: typed_ir::Type,
50        source: TypeSource,
51    },
52    NonRuntimeType {
53        ty: typed_ir::Type,
54        source: TypeSource,
55    },
56    ResidualPolymorphicBinding {
57        path: typed_ir::Path,
58        vars: Vec<typed_ir::TypeVar>,
59        source: ResidualPolymorphicSource,
60    },
61    InvariantViolation {
62        stage: &'static str,
63        context: String,
64        message: &'static str,
65    },
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum TypeSource {
70    BindingScheme,
71    BindingGraph,
72    RootGraph,
73    ApplyEvidence,
74    ApplyCalleeEvidence,
75    ApplyArgumentEvidence,
76    ApplyArgumentSourceEdge,
77    JoinEvidence,
78    Expected,
79    Local,
80    Literal,
81    Structural,
82    Validation,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct TypeMismatchContext {
87    pub callee: Option<RuntimeCalleeLabel>,
88    pub phase: TypeMismatchPhase,
89    pub callee_source_edge: Option<u32>,
90    pub arg_source_edge: Option<u32>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum RuntimeCalleeLabel {
95    Path(typed_ir::Path),
96    Primitive(typed_ir::PrimitiveOp),
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum TypeMismatchPhase {
101    ApplyCallee,
102    ApplyArgument,
103    ApplyResult,
104    Expected,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum ResidualPolymorphicSource {
109    TypeParams,
110    RuntimeTypes,
111}
112
113impl RuntimeError {
114    pub fn with_type_mismatch_context(self, context: TypeMismatchContext) -> Self {
115        match self {
116            RuntimeError::TypeMismatch {
117                expected,
118                actual,
119                source,
120                context: None,
121            } => RuntimeError::TypeMismatch {
122                expected,
123                actual,
124                source,
125                context: Some(context),
126            },
127            other => other,
128        }
129    }
130}
131
132impl ResidualPolymorphicSource {
133    fn description(self) -> &'static str {
134        match self {
135            ResidualPolymorphicSource::TypeParams => "binding type parameters",
136            ResidualPolymorphicSource::RuntimeTypes => "runtime body, scheme, or role requirements",
137        }
138    }
139}
140
141impl fmt::Display for RuntimeError {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        match self {
144            RuntimeError::MissingBindingType { path } => {
145                write!(f, "missing binding type for {}", display_path(path))
146            }
147            RuntimeError::MissingRootType { index } => {
148                write!(
149                    f,
150                    "could not determine the type of expression #{index}. \
151This usually means a name, field, method, or operator could not be resolved."
152                )
153            }
154            RuntimeError::MissingLocalType { path } => {
155                write!(f, "missing local type for {}", display_path(path))
156            }
157            RuntimeError::MissingExpectedType { node } => {
158                write!(f, "missing expected type for {node}")
159            }
160            RuntimeError::MissingApplyEvidence => write!(f, "missing apply evidence"),
161            RuntimeError::MissingJoinEvidence { node } => {
162                write!(f, "missing join evidence for {node}")
163            }
164            RuntimeError::NonFunctionCallee { ty } => {
165                write!(f, "expected a function, but got {}", display_type(ty))
166            }
167            RuntimeError::ExpectedThunk { ty } => {
168                write!(
169                    f,
170                    "expected an effectful computation, but got {}",
171                    display_type(ty)
172                )
173            }
174            RuntimeError::TypeMismatch {
175                expected,
176                actual,
177                source,
178                context,
179            } => {
180                if let Some(context) = context
181                    && let Some(callee) = &context.callee
182                {
183                    let callee = display_callee_label(callee);
184                    let mismatch = match context.phase {
185                        TypeMismatchPhase::ApplyArgument => "argument type mismatch",
186                        TypeMismatchPhase::ApplyCallee => "callee type mismatch",
187                        TypeMismatchPhase::ApplyResult => "result type mismatch",
188                        TypeMismatchPhase::Expected => "type mismatch",
189                    };
190                    return write!(
191                        f,
192                        "{mismatch} in call to `{callee}`: expected {}, got {}",
193                        display_type(expected),
194                        display_type(actual)
195                    );
196                }
197                let context = match source {
198                    TypeSource::ApplyEvidence | TypeSource::ApplyCalleeEvidence => {
199                        "function application"
200                    }
201                    TypeSource::ApplyArgumentEvidence | TypeSource::ApplyArgumentSourceEdge => {
202                        "function argument"
203                    }
204                    TypeSource::JoinEvidence => "branch result",
205                    TypeSource::RootGraph => "top-level expression",
206                    TypeSource::BindingScheme | TypeSource::BindingGraph => "binding",
207                    TypeSource::Local => "local value",
208                    TypeSource::Literal => "literal",
209                    TypeSource::Structural => "structured value",
210                    TypeSource::Validation => "runtime validation",
211                    TypeSource::Expected => "expected type",
212                };
213                write!(
214                    f,
215                    "{context} type mismatch: expected {}, got {}",
216                    display_type(expected),
217                    display_type(actual)
218                )
219            }
220            RuntimeError::UnsupportedPatternShape { pattern, ty } => {
221                write!(
222                    f,
223                    "cannot match a {pattern} pattern against {}",
224                    display_type(ty)
225                )
226            }
227            RuntimeError::UnsupportedSelectBase { field, ty } => {
228                write!(f, "cannot select .{} from {}", field.0, display_type(ty))
229            }
230            RuntimeError::UnboundVariable { path } => {
231                write!(f, "unbound variable {}", display_path(path))
232            }
233            RuntimeError::ResidualAny { ty, source } => {
234                write!(
235                    f,
236                    "runtime type is still unknown after inference ({source:?}): {}",
237                    display_type(ty)
238                )
239            }
240            RuntimeError::NonRuntimeType { ty, source } => {
241                write!(
242                    f,
243                    "type cannot be represented at runtime ({source:?}): {}",
244                    display_type(ty)
245                )
246            }
247            RuntimeError::ResidualPolymorphicBinding { path, vars, source } => {
248                let plural = if vars.len() == 1 { "" } else { "s" };
249                write!(
250                    f,
251                    "cannot infer all runtime types needed for `{}`. \
252                     Add a type annotation that fixes the remaining type \
253                     variable{}: {}. Source: {}.",
254                    display_path(path),
255                    plural,
256                    display_type_vars(vars),
257                    source.description()
258                )
259            }
260            RuntimeError::InvariantViolation {
261                stage,
262                context,
263                message,
264            } => write!(
265                f,
266                "runtime invariant failed after {stage} at {context}: {message}"
267            ),
268        }
269    }
270}
271
272impl std::error::Error for RuntimeError {}
273
274fn display_path(path: &typed_ir::Path) -> String {
275    path.segments
276        .iter()
277        .map(|segment| segment.0.as_str())
278        .collect::<Vec<_>>()
279        .join("::")
280}
281
282fn display_callee_label(label: &RuntimeCalleeLabel) -> String {
283    match label {
284        RuntimeCalleeLabel::Path(path) => display_callee_path(path),
285        RuntimeCalleeLabel::Primitive(op) => display_primitive_op(*op).to_string(),
286    }
287}
288
289fn display_callee_path(path: &typed_ir::Path) -> String {
290    match path.segments.as_slice() {
291        [std, int, add] if std.0 == "std" && int.0 == "int" && add.0 == "add" => "+".to_string(),
292        [std, int, sub] if std.0 == "std" && int.0 == "int" && sub.0 == "sub" => "-".to_string(),
293        [std, int, mul] if std.0 == "std" && int.0 == "int" && mul.0 == "mul" => "*".to_string(),
294        [std, int, div] if std.0 == "std" && int.0 == "int" && div.0 == "div" => "/".to_string(),
295        _ => display_path(path),
296    }
297}
298
299fn display_primitive_op(op: typed_ir::PrimitiveOp) -> &'static str {
300    match op {
301        typed_ir::PrimitiveOp::BoolNot => "not",
302        typed_ir::PrimitiveOp::BoolEq => "==",
303        typed_ir::PrimitiveOp::IntAdd => "+",
304        typed_ir::PrimitiveOp::IntSub => "-",
305        typed_ir::PrimitiveOp::IntMul => "*",
306        typed_ir::PrimitiveOp::IntDiv => "/",
307        typed_ir::PrimitiveOp::IntEq => "==",
308        typed_ir::PrimitiveOp::IntLt => "<",
309        typed_ir::PrimitiveOp::IntLe => "<=",
310        typed_ir::PrimitiveOp::IntGt => ">",
311        typed_ir::PrimitiveOp::IntGe => ">=",
312        typed_ir::PrimitiveOp::FloatAdd => "+",
313        typed_ir::PrimitiveOp::FloatSub => "-",
314        typed_ir::PrimitiveOp::FloatMul => "*",
315        typed_ir::PrimitiveOp::FloatDiv => "/",
316        typed_ir::PrimitiveOp::FloatEq => "==",
317        typed_ir::PrimitiveOp::FloatLt => "<",
318        typed_ir::PrimitiveOp::FloatLe => "<=",
319        typed_ir::PrimitiveOp::FloatGt => ">",
320        typed_ir::PrimitiveOp::FloatGe => ">=",
321        typed_ir::PrimitiveOp::StringEq => "==",
322        typed_ir::PrimitiveOp::StringConcat => "++",
323        typed_ir::PrimitiveOp::ListIndex => "[]",
324        typed_ir::PrimitiveOp::ListIndexRange => "[..]",
325        typed_ir::PrimitiveOp::ListSplice => "splice",
326        typed_ir::PrimitiveOp::StringIndex => "[]",
327        typed_ir::PrimitiveOp::StringIndexRange => "[..]",
328        typed_ir::PrimitiveOp::StringSplice => "splice",
329        _ => primitive_op_name(op),
330    }
331}
332
333fn primitive_op_name(op: typed_ir::PrimitiveOp) -> &'static str {
334    match op {
335        typed_ir::PrimitiveOp::ListEmpty => "list.empty",
336        typed_ir::PrimitiveOp::ListSingleton => "list.singleton",
337        typed_ir::PrimitiveOp::ListLen => "list.len",
338        typed_ir::PrimitiveOp::ListMerge => "list.merge",
339        typed_ir::PrimitiveOp::ListIndexRangeRaw => "list.index_range_raw",
340        typed_ir::PrimitiveOp::ListSpliceRaw => "list.splice_raw",
341        typed_ir::PrimitiveOp::ListViewRaw => "list.view_raw",
342        typed_ir::PrimitiveOp::StringLen => "string.len",
343        typed_ir::PrimitiveOp::StringIndexRangeRaw => "string.index_range_raw",
344        typed_ir::PrimitiveOp::StringSpliceRaw => "string.splice_raw",
345        typed_ir::PrimitiveOp::IntToString => "int.to_string",
346        typed_ir::PrimitiveOp::IntToHex => "int.to_hex",
347        typed_ir::PrimitiveOp::IntToUpperHex => "int.to_upper_hex",
348        typed_ir::PrimitiveOp::FloatToString => "float.to_string",
349        typed_ir::PrimitiveOp::BoolToString => "bool.to_string",
350        _ => "primitive",
351    }
352}
353
354fn display_type_vars(vars: &[typed_ir::TypeVar]) -> String {
355    if vars.is_empty() {
356        return "<none>".to_string();
357    }
358    vars.iter()
359        .map(display_type_var)
360        .collect::<Vec<_>>()
361        .join(", ")
362}
363
364fn display_type_var(var: &typed_ir::TypeVar) -> &str {
365    let name = var.0.as_str();
366    if name
367        .strip_prefix('t')
368        .is_some_and(|rest| !rest.is_empty() && rest.chars().all(|ch| ch.is_ascii_digit()))
369    {
370        "'a"
371    } else {
372        name
373    }
374}
375
376pub fn display_type(ty: &typed_ir::Type) -> String {
377    match ty {
378        typed_ir::Type::Unknown => "?".to_string(),
379        typed_ir::Type::Var(var) => display_type_var(var).to_string(),
380        typed_ir::Type::Never => "never".to_string(),
381        typed_ir::Type::Any => "_".to_string(),
382        typed_ir::Type::Named { path, args } => {
383            let name = display_path(path);
384            if args.is_empty() {
385                name
386            } else {
387                format!(
388                    "{}<{}>",
389                    name,
390                    args.iter()
391                        .map(display_type_arg)
392                        .collect::<Vec<_>>()
393                        .join(", ")
394                )
395            }
396        }
397        typed_ir::Type::Fun {
398            param,
399            param_effect,
400            ret_effect,
401            ret,
402        } => {
403            let param = display_type(param);
404            let param_effect = display_type(param_effect);
405            let ret_effect = display_type(ret_effect);
406            let ret = display_type(ret);
407            if param_effect == "never" && ret_effect == "never" {
408                format!("{param} -> {ret}")
409            } else {
410                format!("{param} -{param_effect} / {ret_effect}-> {ret}")
411            }
412        }
413        typed_ir::Type::Tuple(items) => format!(
414            "({})",
415            items
416                .iter()
417                .map(display_type)
418                .collect::<Vec<_>>()
419                .join(", ")
420        ),
421        typed_ir::Type::Record(record) => {
422            let mut parts = record
423                .fields
424                .iter()
425                .map(|field| {
426                    let optional = if field.optional { "?" } else { "" };
427                    format!(
428                        "{}{}: {}",
429                        field.name.0,
430                        optional,
431                        display_type(&field.value)
432                    )
433                })
434                .collect::<Vec<_>>();
435            match &record.spread {
436                Some(typed_ir::RecordSpread::Head(rest))
437                | Some(typed_ir::RecordSpread::Tail(rest)) => {
438                    parts.push(format!("..{}", display_type(rest)));
439                }
440                None => {}
441            }
442            format!("{{{}}}", parts.join(", "))
443        }
444        typed_ir::Type::Variant(variant) => {
445            let mut parts = variant
446                .cases
447                .iter()
448                .map(|case| {
449                    if case.payloads.is_empty() {
450                        case.name.0.clone()
451                    } else {
452                        format!(
453                            "{}({})",
454                            case.name.0,
455                            case.payloads
456                                .iter()
457                                .map(display_type)
458                                .collect::<Vec<_>>()
459                                .join(", ")
460                        )
461                    }
462                })
463                .collect::<Vec<_>>();
464            if let Some(rest) = &variant.tail {
465                parts.push(format!("..{}", display_type(rest)));
466            }
467            format!("[{}]", parts.join(" | "))
468        }
469        typed_ir::Type::Row { items, tail } => {
470            let mut parts = items.iter().map(display_type).collect::<Vec<_>>();
471            parts.push(format!("..{}", display_type(tail)));
472            format!("[{}]", parts.join("; "))
473        }
474        typed_ir::Type::Union(items) => items
475            .iter()
476            .map(display_type)
477            .collect::<Vec<_>>()
478            .join(" | "),
479        typed_ir::Type::Inter(items) => items
480            .iter()
481            .map(display_type)
482            .collect::<Vec<_>>()
483            .join(" & "),
484        typed_ir::Type::Recursive { var, body } => {
485            format!("rec {}. {}", var.0, display_type(body))
486        }
487    }
488}
489
490fn display_type_arg(arg: &typed_ir::TypeArg) -> String {
491    match arg {
492        typed_ir::TypeArg::Type(ty) => display_type(ty),
493        typed_ir::TypeArg::Bounds(bounds) => display_type_bounds(bounds),
494    }
495}
496
497fn display_type_bounds(bounds: &typed_ir::TypeBounds) -> String {
498    match (&bounds.lower, &bounds.upper) {
499        (Some(lower), Some(upper)) if lower == upper => display_type(lower),
500        (Some(lower), Some(upper)) => format!("{}..{}", display_type(lower), display_type(upper)),
501        (Some(lower), None) => format!("{}..", display_type(lower)),
502        (None, Some(upper)) => format!("..{}", display_type(upper)),
503        (None, None) => "_".to_string(),
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    #[test]
512    fn displays_apply_type_mismatch_without_debug_type_dump() {
513        let error = RuntimeError::TypeMismatch {
514            expected: fun_type(named_type("bool"), named_type("bool")),
515            actual: fun_type(named_type("int"), named_type("int")),
516            source: TypeSource::ApplyEvidence,
517            context: None,
518        };
519
520        assert_eq!(
521            error.to_string(),
522            "function application type mismatch: expected bool -> bool, got int -> int"
523        );
524    }
525
526    #[test]
527    fn displays_apply_type_mismatch_with_callee_context() {
528        let error = RuntimeError::TypeMismatch {
529            expected: fun_type(named_type("bool"), named_type("bool")),
530            actual: fun_type(named_type("int"), named_type("int")),
531            source: TypeSource::ApplyEvidence,
532            context: Some(TypeMismatchContext {
533                callee: Some(RuntimeCalleeLabel::Path(typed_ir::Path {
534                    segments: vec![
535                        typed_ir::Name("std".to_string()),
536                        typed_ir::Name("int".to_string()),
537                        typed_ir::Name("add".to_string()),
538                    ],
539                })),
540                phase: TypeMismatchPhase::ApplyResult,
541                callee_source_edge: Some(1),
542                arg_source_edge: Some(2),
543            }),
544        };
545
546        assert_eq!(
547            error.to_string(),
548            "result type mismatch in call to `+`: expected bool -> bool, got int -> int"
549        );
550    }
551
552    #[test]
553    fn displays_missing_root_type_as_surface_inference_failure() {
554        let error = RuntimeError::MissingRootType { index: 0 };
555
556        assert_eq!(
557            error.to_string(),
558            "could not determine the type of expression #0. This usually means a name, field, method, or operator could not be resolved."
559        );
560    }
561
562    #[test]
563    fn displays_residual_polymorphic_source() {
564        let error = RuntimeError::ResidualPolymorphicBinding {
565            path: typed_ir::Path::from_name(typed_ir::Name("f".to_string())),
566            vars: vec![typed_ir::TypeVar("a".to_string())],
567            source: ResidualPolymorphicSource::RuntimeTypes,
568        };
569
570        assert_eq!(
571            error.to_string(),
572            "cannot infer all runtime types needed for `f`. \
573             Add a type annotation that fixes the remaining type variable: a. \
574             Source: runtime body, scheme, or role requirements."
575        );
576    }
577
578    #[test]
579    fn displays_internal_type_vars_as_user_type_vars() {
580        let error = RuntimeError::ResidualPolymorphicBinding {
581            path: typed_ir::Path::from_name(typed_ir::Name("wrap".to_string())),
582            vars: vec![typed_ir::TypeVar("t4230".to_string())],
583            source: ResidualPolymorphicSource::TypeParams,
584        };
585
586        assert_eq!(
587            error.to_string(),
588            "cannot infer all runtime types needed for `wrap`. \
589             Add a type annotation that fixes the remaining type variable: 'a. \
590             Source: binding type parameters."
591        );
592    }
593
594    fn fun_type(param: typed_ir::Type, ret: typed_ir::Type) -> typed_ir::Type {
595        typed_ir::Type::Fun {
596            param: Box::new(param),
597            param_effect: Box::new(typed_ir::Type::Never),
598            ret_effect: Box::new(typed_ir::Type::Never),
599            ret: Box::new(ret),
600        }
601    }
602
603    fn named_type(name: &str) -> typed_ir::Type {
604        typed_ir::Type::Named {
605            path: typed_ir::Path::from_name(typed_ir::Name(name.to_string())),
606            args: Vec::new(),
607        }
608    }
609}