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}