1use crate::ast::BinOp;
11use crate::span::Span;
12use crate::token::TokenKind;
13
14#[derive(Debug, Clone, PartialEq)]
21pub enum QalaError {
22 UnterminatedString { span: Span },
27
28 UnterminatedInterpolation { span: Span },
31
32 InvalidEscape { span: Span, message: String },
36
37 UnexpectedChar { span: Span, ch: char },
42
43 IntOverflow { span: Span },
46
47 MalformedNumber { span: Span, message: String },
51
52 BadByteLiteral { span: Span, message: String },
56
57 UnexpectedToken {
63 span: Span,
64 expected: Vec<TokenKind>,
65 found: TokenKind,
66 },
67
68 UnclosedDelimiter {
73 span: Span,
74 opener: TokenKind,
75 found: TokenKind,
76 },
77
78 UnexpectedEof {
83 span: Span,
84 expected: Vec<TokenKind>,
85 },
86
87 Parse { span: Span, message: String },
92
93 TypeMismatch {
101 span: Span,
102 expected: String,
103 found: String,
104 },
105
106 MissingReturn {
112 span: Span,
113 fn_name: String,
114 expected: String,
115 },
116
117 UndefinedName { span: Span, name: String },
122
123 UnknownType { span: Span, name: String },
128
129 RecursiveStructByValue { span: Span, path: Vec<String> },
136
137 NonExhaustiveMatch {
143 span: Span,
144 enum_name: String,
145 missing: Vec<String>,
146 },
147
148 InterfaceNotSatisfied {
156 span: Span,
157 ty: String,
158 interface: String,
159 missing: Vec<String>,
160 mismatched: Vec<(String, String, String)>,
161 },
162
163 EffectViolation {
170 span: Span,
171 caller: String,
172 caller_effect: String,
173 callee: String,
174 callee_effect: String,
175 },
176
177 RedundantQuestionOperator { span: Span, message: String },
185
186 Type { span: Span, message: String },
194
195 IntegerOverflow {
206 span: Span,
207 op: BinOp,
208 lhs: i64,
209 rhs: i64,
210 },
211
212 ComptimeBudgetExceeded { span: Span },
218
219 ComptimeEffectViolation {
229 span: Span,
230 fn_name: String,
231 effect: String,
232 },
233
234 ComptimeResultNotConstable { span: Span, type_name: String },
241
242 Runtime { span: Span, message: String },
255}
256
257impl QalaError {
258 pub fn span(&self) -> Span {
264 match self {
265 QalaError::UnterminatedString { span }
266 | QalaError::UnterminatedInterpolation { span }
267 | QalaError::InvalidEscape { span, .. }
268 | QalaError::UnexpectedChar { span, .. }
269 | QalaError::IntOverflow { span }
270 | QalaError::MalformedNumber { span, .. }
271 | QalaError::BadByteLiteral { span, .. }
272 | QalaError::UnexpectedToken { span, .. }
273 | QalaError::UnclosedDelimiter { span, .. }
274 | QalaError::UnexpectedEof { span, .. }
275 | QalaError::Parse { span, .. }
276 | QalaError::TypeMismatch { span, .. }
277 | QalaError::MissingReturn { span, .. }
278 | QalaError::UndefinedName { span, .. }
279 | QalaError::UnknownType { span, .. }
280 | QalaError::RecursiveStructByValue { span, .. }
281 | QalaError::NonExhaustiveMatch { span, .. }
282 | QalaError::InterfaceNotSatisfied { span, .. }
283 | QalaError::EffectViolation { span, .. }
284 | QalaError::RedundantQuestionOperator { span, .. }
285 | QalaError::Type { span, .. }
286 | QalaError::IntegerOverflow { span, .. }
287 | QalaError::ComptimeBudgetExceeded { span }
288 | QalaError::ComptimeEffectViolation { span, .. }
289 | QalaError::ComptimeResultNotConstable { span, .. }
290 | QalaError::Runtime { span, .. } => *span,
291 }
292 }
293
294 pub fn message(&self) -> String {
300 match self {
301 QalaError::UnterminatedString { .. } => "unterminated string literal".to_string(),
302 QalaError::UnterminatedInterpolation { .. } => {
303 "unterminated interpolation: missing `}`".to_string()
304 }
305 QalaError::InvalidEscape { message, .. } => {
306 format!("invalid escape sequence: {message}")
307 }
308 QalaError::UnexpectedChar { ch, .. } => {
309 format!("unexpected character {ch:?}")
310 }
311 QalaError::IntOverflow { .. } => "integer literal is too large for i64".to_string(),
312 QalaError::MalformedNumber { message, .. } => {
313 format!("malformed number literal: {message}")
314 }
315 QalaError::BadByteLiteral { message, .. } => {
316 format!("malformed byte literal: {message}")
317 }
318 QalaError::UnexpectedToken {
319 expected, found, ..
320 } => {
321 format!(
322 "expected {}, found {}",
323 expected_list(expected),
324 display_kind(found)
325 )
326 }
327 QalaError::UnclosedDelimiter { opener, found, .. } => {
328 format!(
329 "unclosed {} -- found {}",
330 display_kind(opener),
331 display_kind(found)
332 )
333 }
334 QalaError::UnexpectedEof { expected, .. } => {
335 format!(
336 "unexpected end of input, expected {}",
337 expected_list(expected)
338 )
339 }
340 QalaError::Parse { message, .. } => message.clone(),
341 QalaError::TypeMismatch {
342 expected, found, ..
343 } => {
344 format!("expected {expected}, found {found}")
345 }
346 QalaError::MissingReturn {
347 fn_name, expected, ..
348 } => {
349 format!(
350 "function `{fn_name}` is declared to return {expected} but its body has no value"
351 )
352 }
353 QalaError::UndefinedName { name, .. } => {
354 format!("undefined name `{name}`")
355 }
356 QalaError::UnknownType { name, .. } => {
357 format!("unknown type `{name}`")
358 }
359 QalaError::RecursiveStructByValue { path, .. } => {
360 format!("recursive struct: {}", path.join(" -> "))
361 }
362 QalaError::NonExhaustiveMatch {
363 enum_name, missing, ..
364 } => {
365 format!(
366 "non-exhaustive match on enum `{enum_name}`: missing variants: {}",
367 missing.join(", ")
368 )
369 }
370 QalaError::InterfaceNotSatisfied { ty, interface, .. } => {
371 format!("type `{ty}` does not satisfy interface `{interface}`")
372 }
373 QalaError::EffectViolation {
374 caller,
375 caller_effect,
376 callee,
377 callee_effect,
378 ..
379 } => {
380 format!(
381 "{caller_effect} function `{caller}` calls {callee_effect} function `{callee}`"
382 )
383 }
384 QalaError::RedundantQuestionOperator { message, .. } => message.clone(),
385 QalaError::Type { message, .. } => message.clone(),
386 QalaError::IntegerOverflow { op, lhs, rhs, .. } => {
387 format!(
388 "integer overflow: {lhs} {} {rhs} does not fit in i64",
389 op_symbol(op)
390 )
391 }
392 QalaError::ComptimeBudgetExceeded { .. } => {
393 "comptime evaluation exceeded 100000-instruction budget".to_string()
394 }
395 QalaError::ComptimeEffectViolation {
396 fn_name, effect, ..
397 } => {
398 format!("comptime block calls {effect} function `{fn_name}`")
399 }
400 QalaError::ComptimeResultNotConstable { type_name, .. } => {
401 format!(
402 "comptime result of type `{type_name}` is not representable as a constant (only primitives and strings)"
403 )
404 }
405 QalaError::Runtime { message, .. } => message.clone(),
406 }
407 }
408}
409
410pub fn display_kind(kind: &TokenKind) -> &'static str {
418 use TokenKind::*;
419 match kind {
420 Int(_) => "an integer literal",
422 Float(_) => "a float literal",
423 Byte(_) => "a byte literal",
424 Str(_) => "a string literal",
425 StrStart(_) => "the start of a string",
426 StrMid(_) => "more string text",
427 StrEnd(_) => "the end of a string",
428 InterpStart => "`{` (interpolation start)",
429 InterpEnd => "`}` (interpolation end)",
430 Ident(_) => "an identifier",
431 Fn => "`fn`",
433 Let => "`let`",
434 Mut => "`mut`",
435 If => "`if`",
436 Else => "`else`",
437 While => "`while`",
438 For => "`for`",
439 In => "`in`",
440 Return => "`return`",
441 Break => "`break`",
442 Continue => "`continue`",
443 Defer => "`defer`",
444 Match => "`match`",
445 Struct => "`struct`",
446 Enum => "`enum`",
447 Interface => "`interface`",
448 Comptime => "`comptime`",
449 Is => "`is`",
450 Pure => "`pure`",
451 Io => "`io`",
452 Alloc => "`alloc`",
453 Panic => "`panic`",
454 Or => "`or`",
455 SelfKw => "`self`",
456 True => "`true`",
457 False => "`false`",
458 I64Ty => "`i64`",
460 F64Ty => "`f64`",
461 BoolTy => "`bool`",
462 StrTy => "`str`",
463 ByteTy => "`byte`",
464 VoidTy => "`void`",
465 Plus => "`+`",
467 Minus => "`-`",
468 Star => "`*`",
469 Slash => "`/`",
470 Percent => "`%`",
471 EqEq => "`==`",
472 BangEq => "`!=`",
473 Lt => "`<`",
474 LtEq => "`<=`",
475 Gt => "`>`",
476 GtEq => "`>=`",
477 AmpAmp => "`&&`",
478 PipePipe => "`||`",
479 Bang => "`!`",
480 Eq => "`=`",
481 Dot => "`.`",
482 Comma => "`,`",
483 Colon => "`:`",
484 Semi => "`;`",
485 LParen => "`(`",
486 RParen => "`)`",
487 LBracket => "`[`",
488 RBracket => "`]`",
489 LBrace => "`{`",
490 RBrace => "`}`",
491 Arrow => "`->`",
492 FatArrow => "`=>`",
493 PipeGt => "`|>`",
494 Question => "`?`",
495 DotDot => "`..`",
496 DotDotEq => "`..=`",
497 Eof => "end of input",
498 }
499}
500
501fn expected_list(expected: &[TokenKind]) -> String {
505 let names: Vec<&'static str> = expected.iter().map(display_kind).collect();
506 match names.as_slice() {
507 [] => "something else".to_string(),
508 [only] => only.to_string(),
509 [a, b] => format!("{a} or {b}"),
510 [head @ .., last] => format!("{}, or {}", head.join(", "), last),
511 }
512}
513
514fn op_symbol(op: &BinOp) -> &'static str {
522 match op {
523 BinOp::Add => "+",
524 BinOp::Sub => "-",
525 BinOp::Mul => "*",
526 BinOp::Div => "/",
527 BinOp::Rem => "%",
528 BinOp::Eq
529 | BinOp::Ne
530 | BinOp::Lt
531 | BinOp::Le
532 | BinOp::Gt
533 | BinOp::Ge
534 | BinOp::And
535 | BinOp::Or => "?",
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 fn sample(n: usize) -> Span {
545 Span::new(n, n + 1)
546 }
547
548 #[test]
549 fn span_returns_the_carried_span_for_every_lex_variant() {
550 let cases: Vec<(QalaError, Span)> = vec![
551 (QalaError::UnterminatedString { span: sample(1) }, sample(1)),
552 (
553 QalaError::UnterminatedInterpolation { span: sample(2) },
554 sample(2),
555 ),
556 (
557 QalaError::InvalidEscape {
558 span: sample(3),
559 message: "\\q".to_string(),
560 },
561 sample(3),
562 ),
563 (
564 QalaError::UnexpectedChar {
565 span: sample(4),
566 ch: '\u{00e9}',
567 },
568 sample(4),
569 ),
570 (QalaError::IntOverflow { span: sample(5) }, sample(5)),
571 (
572 QalaError::MalformedNumber {
573 span: sample(6),
574 message: "1_".to_string(),
575 },
576 sample(6),
577 ),
578 (
579 QalaError::BadByteLiteral {
580 span: sample(7),
581 message: "b''".to_string(),
582 },
583 sample(7),
584 ),
585 ];
586 for (err, expected) in cases {
589 assert_eq!(err.span(), expected, "span() mismatch for {err:?}");
590 }
591 }
592
593 #[test]
594 fn span_returns_the_carried_span_for_every_parse_variant() {
595 let cases: Vec<(QalaError, Span)> = vec![
596 (
597 QalaError::UnexpectedToken {
598 span: sample(1),
599 expected: vec![TokenKind::RParen],
600 found: TokenKind::RBracket,
601 },
602 sample(1),
603 ),
604 (
605 QalaError::UnclosedDelimiter {
606 span: sample(2),
607 opener: TokenKind::LParen,
608 found: TokenKind::RBrace,
609 },
610 sample(2),
611 ),
612 (
613 QalaError::UnexpectedEof {
614 span: sample(3),
615 expected: vec![TokenKind::Semi],
616 },
617 sample(3),
618 ),
619 (
620 QalaError::Parse {
621 span: sample(4),
622 message: "expression nests too deeply".to_string(),
623 },
624 sample(4),
625 ),
626 ];
627 for (err, expected) in cases {
628 assert_eq!(err.span(), expected, "span() mismatch for {err:?}");
629 }
630 }
631
632 #[test]
633 fn span_returns_the_carried_span_for_every_type_variant() {
634 let cases: Vec<(QalaError, Span)> = vec![
635 (
636 QalaError::TypeMismatch {
637 span: sample(1),
638 expected: "i64".to_string(),
639 found: "str".to_string(),
640 },
641 sample(1),
642 ),
643 (
644 QalaError::MissingReturn {
645 span: sample(2),
646 fn_name: "f".to_string(),
647 expected: "i64".to_string(),
648 },
649 sample(2),
650 ),
651 (
652 QalaError::UndefinedName {
653 span: sample(3),
654 name: "x".to_string(),
655 },
656 sample(3),
657 ),
658 (
659 QalaError::UnknownType {
660 span: sample(4),
661 name: "Shape".to_string(),
662 },
663 sample(4),
664 ),
665 (
666 QalaError::RecursiveStructByValue {
667 span: sample(5),
668 path: vec!["A".to_string(), "B".to_string(), "A".to_string()],
669 },
670 sample(5),
671 ),
672 (
673 QalaError::NonExhaustiveMatch {
674 span: sample(6),
675 enum_name: "Dir".to_string(),
676 missing: vec!["Bar".to_string(), "Foo".to_string()],
677 },
678 sample(6),
679 ),
680 (
681 QalaError::InterfaceNotSatisfied {
682 span: sample(7),
683 ty: "Point".to_string(),
684 interface: "Printable".to_string(),
685 missing: vec!["to_string".to_string()],
686 mismatched: vec![],
687 },
688 sample(7),
689 ),
690 (
691 QalaError::EffectViolation {
692 span: sample(8),
693 caller: "compute".to_string(),
694 caller_effect: "pure".to_string(),
695 callee: "println".to_string(),
696 callee_effect: "io".to_string(),
697 },
698 sample(8),
699 ),
700 (
701 QalaError::RedundantQuestionOperator {
702 span: sample(9),
703 message: "`?` outside a Result-returning function".to_string(),
704 },
705 sample(9),
706 ),
707 (
708 QalaError::Type {
709 span: sample(10),
710 message: "literal pattern cannot match an enum value".to_string(),
711 },
712 sample(10),
713 ),
714 ];
715 for (err, expected) in cases {
716 assert_eq!(err.span(), expected, "span() mismatch for {err:?}");
717 }
718 }
719
720 #[test]
721 fn span_returns_the_carried_span_for_the_runtime_variant() {
722 let e = QalaError::Runtime {
725 span: sample(7),
726 message: "division by zero".to_string(),
727 };
728 assert_eq!(e.span(), sample(7), "span() mismatch for {e:?}");
729 }
730
731 #[test]
732 fn runtime_message_returns_the_carried_message_verbatim() {
733 let e = QalaError::Runtime {
736 span: sample(0),
737 message: "index 5 out of bounds for length 3".to_string(),
738 };
739 assert_eq!(e.message(), "index 5 out of bounds for length 3");
740 }
741
742 #[test]
743 fn errors_are_comparable_and_clonable() {
744 let a = QalaError::IntOverflow { span: sample(0) };
745 let b = a.clone();
746 assert_eq!(a, b);
747 let c = QalaError::IntOverflow { span: sample(1) };
748 assert_ne!(a, c);
749 }
750
751 #[test]
752 fn message_is_a_plain_nonempty_line_per_variant() {
753 let errs = [
754 QalaError::UnterminatedString { span: sample(0) },
755 QalaError::UnterminatedInterpolation { span: sample(0) },
756 QalaError::InvalidEscape {
757 span: sample(0),
758 message: "\\q".to_string(),
759 },
760 QalaError::UnexpectedChar {
761 span: sample(0),
762 ch: '@',
763 },
764 QalaError::IntOverflow { span: sample(0) },
765 QalaError::MalformedNumber {
766 span: sample(0),
767 message: "1e".to_string(),
768 },
769 QalaError::BadByteLiteral {
770 span: sample(0),
771 message: "b''".to_string(),
772 },
773 QalaError::UnexpectedToken {
774 span: sample(0),
775 expected: vec![TokenKind::Comma, TokenKind::RParen],
776 found: TokenKind::RBracket,
777 },
778 QalaError::UnclosedDelimiter {
779 span: sample(0),
780 opener: TokenKind::LParen,
781 found: TokenKind::Eof,
782 },
783 QalaError::UnexpectedEof {
784 span: sample(0),
785 expected: vec![TokenKind::Ident(String::new())],
786 },
787 QalaError::Parse {
788 span: sample(0),
789 message: "expression nests too deeply".to_string(),
790 },
791 QalaError::TypeMismatch {
792 span: sample(0),
793 expected: "i64".to_string(),
794 found: "str".to_string(),
795 },
796 QalaError::MissingReturn {
797 span: sample(0),
798 fn_name: "f".to_string(),
799 expected: "i64".to_string(),
800 },
801 QalaError::UndefinedName {
802 span: sample(0),
803 name: "x".to_string(),
804 },
805 QalaError::UnknownType {
806 span: sample(0),
807 name: "Shape".to_string(),
808 },
809 QalaError::RecursiveStructByValue {
810 span: sample(0),
811 path: vec!["A".to_string(), "B".to_string(), "A".to_string()],
812 },
813 QalaError::NonExhaustiveMatch {
814 span: sample(0),
815 enum_name: "Dir".to_string(),
816 missing: vec!["Bar".to_string(), "Foo".to_string()],
817 },
818 QalaError::InterfaceNotSatisfied {
819 span: sample(0),
820 ty: "Point".to_string(),
821 interface: "Printable".to_string(),
822 missing: vec!["to_string".to_string()],
823 mismatched: vec![],
824 },
825 QalaError::EffectViolation {
826 span: sample(0),
827 caller: "compute".to_string(),
828 caller_effect: "pure".to_string(),
829 callee: "println".to_string(),
830 callee_effect: "io".to_string(),
831 },
832 QalaError::RedundantQuestionOperator {
833 span: sample(0),
834 message: "`?` outside a Result-returning function".to_string(),
835 },
836 QalaError::Type {
837 span: sample(0),
838 message: "variant `Square` is not part of enum `Shape`".to_string(),
839 },
840 QalaError::IntegerOverflow {
841 span: sample(0),
842 op: BinOp::Mul,
843 lhs: i64::MAX,
844 rhs: 2,
845 },
846 QalaError::ComptimeBudgetExceeded { span: sample(0) },
847 QalaError::ComptimeEffectViolation {
848 span: sample(0),
849 fn_name: "println".to_string(),
850 effect: "io".to_string(),
851 },
852 QalaError::ComptimeResultNotConstable {
853 span: sample(0),
854 type_name: "[i64; 3]".to_string(),
855 },
856 QalaError::Runtime {
857 span: sample(0),
858 message: "division by zero".to_string(),
859 },
860 ];
861 for e in &errs {
862 let m = e.message();
863 assert!(!m.is_empty());
864 assert!(!m.contains('\n'), "message should be one line: {m:?}");
865 }
866 }
867
868 #[test]
869 fn type_mismatch_message_uses_expected_vs_found_template() {
870 let e = QalaError::TypeMismatch {
871 span: sample(0),
872 expected: "i64".to_string(),
873 found: "str".to_string(),
874 };
875 assert_eq!(e.message(), "expected i64, found str");
876 }
877
878 #[test]
879 fn effect_violation_message_uses_locked_template() {
880 let e = QalaError::EffectViolation {
881 span: sample(0),
882 caller: "compute".to_string(),
883 caller_effect: "pure".to_string(),
884 callee: "println".to_string(),
885 callee_effect: "io".to_string(),
886 };
887 let m = e.message();
888 assert!(
890 m.contains("pure function `compute` calls io function `println`"),
891 "effect violation message drift: {m:?}"
892 );
893 }
894
895 #[test]
896 fn recursive_struct_message_joins_path_with_arrows() {
897 let e = QalaError::RecursiveStructByValue {
898 span: sample(0),
899 path: vec!["A".to_string(), "B".to_string(), "A".to_string()],
900 };
901 let m = e.message();
902 assert!(
903 m.contains("A -> B -> A"),
904 "missing arrow-joined path: {m:?}"
905 );
906 assert!(m.starts_with("recursive struct: "), "missing prefix: {m:?}");
908 }
909
910 #[test]
911 fn non_exhaustive_match_message_lists_missing_variants() {
912 let e = QalaError::NonExhaustiveMatch {
914 span: sample(0),
915 enum_name: "Dir".to_string(),
916 missing: vec!["Bar".to_string(), "Foo".to_string()],
917 };
918 let m = e.message();
919 assert!(m.contains("missing variants: Bar, Foo"), "{m:?}");
920 assert!(m.contains("`Dir`"), "missing enum name: {m:?}");
921 }
922
923 #[test]
924 fn interface_not_satisfied_message_names_type_and_interface() {
925 let e = QalaError::InterfaceNotSatisfied {
926 span: sample(0),
927 ty: "Point".to_string(),
928 interface: "Printable".to_string(),
929 missing: vec!["to_string".to_string()],
930 mismatched: vec![(
931 "render".to_string(),
932 "fn(self) -> str".to_string(),
933 "fn(self) -> i64".to_string(),
934 )],
935 };
936 let m = e.message();
937 assert!(m.contains("`Point`"), "missing type name: {m:?}");
938 assert!(m.contains("`Printable`"), "missing interface name: {m:?}");
939 assert!(!m.contains('\n'), "message should be one line: {m:?}");
942 match &e {
945 QalaError::InterfaceNotSatisfied {
946 missing,
947 mismatched,
948 ..
949 } => {
950 assert_eq!(missing, &vec!["to_string".to_string()]);
951 assert_eq!(mismatched.len(), 1);
952 assert_eq!(mismatched[0].0, "render");
953 assert_eq!(mismatched[0].1, "fn(self) -> str");
954 assert_eq!(mismatched[0].2, "fn(self) -> i64");
955 }
956 _ => unreachable!(),
957 }
958 }
959
960 #[test]
961 fn unexpected_token_message_lists_every_expected_kind_and_the_found_one() {
962 let e = QalaError::UnexpectedToken {
963 span: sample(0),
964 expected: vec![TokenKind::Comma, TokenKind::RParen],
965 found: TokenKind::RBracket,
966 };
967 let m = e.message();
968 assert!(m.contains("`,`"), "missing first expected kind: {m:?}");
970 assert!(m.contains("`)`"), "missing second expected kind: {m:?}");
971 assert!(m.contains("found"), "missing the word `found`: {m:?}");
972 assert!(m.contains("`]`"), "missing the found kind: {m:?}");
973 }
974
975 #[test]
976 fn unclosed_delimiter_message_names_the_opener_and_the_surprise() {
977 let e = QalaError::UnclosedDelimiter {
979 span: sample(0),
980 opener: TokenKind::LParen,
981 found: TokenKind::RBrace,
982 };
983 let m = e.message();
984 assert!(m.contains("`(`"), "missing the opener: {m:?}");
985 assert!(m.contains("`}`"), "missing the surprising token: {m:?}");
986 let e = QalaError::UnclosedDelimiter {
988 span: sample(0),
989 opener: TokenKind::LBracket,
990 found: TokenKind::Eof,
991 };
992 let m = e.message();
993 assert!(m.contains("`[`"));
994 assert!(m.contains("end of input"));
995 assert!(!m.contains("Eof"));
996 }
997
998 #[test]
999 fn eof_is_named_end_of_input_in_messages() {
1000 let e = QalaError::UnexpectedEof {
1001 span: sample(0),
1002 expected: vec![TokenKind::Semi],
1003 };
1004 let m = e.message();
1005 assert!(m.contains("end of input"), "{m:?}");
1006 assert!(m.contains("`;`"), "{m:?}");
1007 }
1008
1009 #[test]
1010 fn display_kind_spells_symbols_and_categories() {
1011 assert_eq!(display_kind(&TokenKind::RParen), "`)`");
1012 assert_eq!(display_kind(&TokenKind::LBrace), "`{`");
1013 assert_eq!(display_kind(&TokenKind::Plus), "`+`");
1014 assert_eq!(display_kind(&TokenKind::Eof), "end of input");
1015 assert_eq!(
1016 display_kind(&TokenKind::Ident("x".to_string())),
1017 "an identifier"
1018 );
1019 assert_eq!(display_kind(&TokenKind::Int(7)), "an integer literal");
1020 assert_eq!(display_kind(&TokenKind::Fn), "`fn`");
1021 }
1022
1023 #[test]
1024 fn expected_list_joins_one_two_or_many() {
1025 assert_eq!(expected_list(&[TokenKind::RParen]), "`)`");
1026 assert_eq!(
1027 expected_list(&[TokenKind::Comma, TokenKind::RParen]),
1028 "`,` or `)`"
1029 );
1030 assert_eq!(
1031 expected_list(&[TokenKind::Comma, TokenKind::RParen, TokenKind::RBracket]),
1032 "`,`, `)`, or `]`"
1033 );
1034 assert_eq!(expected_list(&[]), "something else");
1036 }
1037
1038 #[test]
1039 fn span_returns_the_carried_span_for_every_codegen_variant() {
1040 let cases: Vec<(QalaError, Span)> = vec![
1041 (
1042 QalaError::IntegerOverflow {
1043 span: sample(1),
1044 op: BinOp::Mul,
1045 lhs: i64::MAX,
1046 rhs: 2,
1047 },
1048 sample(1),
1049 ),
1050 (
1051 QalaError::ComptimeBudgetExceeded { span: sample(2) },
1052 sample(2),
1053 ),
1054 (
1055 QalaError::ComptimeEffectViolation {
1056 span: sample(3),
1057 fn_name: "println".to_string(),
1058 effect: "io".to_string(),
1059 },
1060 sample(3),
1061 ),
1062 (
1063 QalaError::ComptimeResultNotConstable {
1064 span: sample(4),
1065 type_name: "[i64; 3]".to_string(),
1066 },
1067 sample(4),
1068 ),
1069 ];
1070 for (err, expected) in cases {
1071 assert_eq!(err.span(), expected, "span() mismatch for {err:?}");
1072 }
1073 }
1074
1075 #[test]
1076 fn integer_overflow_message_renders_mul_with_locked_wording() {
1077 let e = QalaError::IntegerOverflow {
1078 span: sample(0),
1079 op: BinOp::Mul,
1080 lhs: i64::MAX,
1081 rhs: 2,
1082 };
1083 assert_eq!(
1084 e.message(),
1085 "integer overflow: 9223372036854775807 * 2 does not fit in i64"
1086 );
1087 }
1088
1089 #[test]
1090 fn integer_overflow_message_renders_add_with_locked_wording() {
1091 let e = QalaError::IntegerOverflow {
1092 span: sample(0),
1093 op: BinOp::Add,
1094 lhs: i64::MAX,
1095 rhs: 1,
1096 };
1097 assert_eq!(
1098 e.message(),
1099 "integer overflow: 9223372036854775807 + 1 does not fit in i64"
1100 );
1101 }
1102
1103 #[test]
1104 fn integer_overflow_message_renders_sub_with_locked_wording() {
1105 let e = QalaError::IntegerOverflow {
1106 span: sample(0),
1107 op: BinOp::Sub,
1108 lhs: i64::MIN,
1109 rhs: 1,
1110 };
1111 assert_eq!(
1112 e.message(),
1113 "integer overflow: -9223372036854775808 - 1 does not fit in i64"
1114 );
1115 }
1116
1117 #[test]
1118 fn comptime_budget_exceeded_message_uses_locked_wording() {
1119 let e = QalaError::ComptimeBudgetExceeded { span: sample(0) };
1120 assert_eq!(
1121 e.message(),
1122 "comptime evaluation exceeded 100000-instruction budget"
1123 );
1124 }
1125
1126 #[test]
1127 fn comptime_effect_violation_message_quotes_only_the_fn_name() {
1128 let e = QalaError::ComptimeEffectViolation {
1129 span: sample(0),
1130 fn_name: "println".to_string(),
1131 effect: "io".to_string(),
1132 };
1133 assert_eq!(e.message(), "comptime block calls io function `println`");
1135 }
1136
1137 #[test]
1138 fn comptime_result_not_constable_message_quotes_the_type_name() {
1139 let e = QalaError::ComptimeResultNotConstable {
1140 span: sample(0),
1141 type_name: "[i64; 3]".to_string(),
1142 };
1143 assert_eq!(
1144 e.message(),
1145 "comptime result of type `[i64; 3]` is not representable as a constant (only primitives and strings)"
1146 );
1147 }
1148
1149 #[test]
1150 fn op_symbol_maps_arithmetic_binops_and_falls_back_for_others() {
1151 assert_eq!(op_symbol(&BinOp::Add), "+");
1152 assert_eq!(op_symbol(&BinOp::Sub), "-");
1153 assert_eq!(op_symbol(&BinOp::Mul), "*");
1154 assert_eq!(op_symbol(&BinOp::Div), "/");
1155 assert_eq!(op_symbol(&BinOp::Rem), "%");
1156 assert_eq!(op_symbol(&BinOp::Eq), "?");
1159 }
1160}