1use crate::ast;
25use crate::effects::EffectSet;
26use crate::errors::QalaError;
27#[allow(unused_imports)] use crate::parser::MAX_DEPTH;
29use crate::span::{LineIndex, Span};
30use crate::typed_ast;
31use crate::types::{QalaType, Symbol};
32use std::collections::{BTreeMap, BTreeSet, HashMap};
33
34#[derive(Debug, Clone, PartialEq)]
45pub struct QalaWarning {
46 pub category: String,
48 pub message: String,
50 pub span: Span,
52 pub note: Option<String>,
55}
56
57pub fn check_program(
68 ast: &ast::Ast,
69 src: &str,
70) -> (typed_ast::TypedAst, Vec<QalaError>, Vec<QalaWarning>) {
71 let mut c = Checker::new(src);
72 c.allow = scan_allow_directives(src);
75 c.preregister_type_names(ast);
81 for item in ast {
83 c.collect_item(item);
84 }
85 c.detect_recursive_structs();
87 let typed: typed_ast::TypedAst = ast.iter().map(|item| c.check_item(item)).collect();
91 c.resolve_function_effects();
93 c.errors.sort_by_key(|e| (e.span().start, e.span().len));
95 c.warnings.sort_by_key(|w| (w.span.start, w.span.len));
96 (typed, c.errors, c.warnings)
97}
98
99#[derive(Default)]
107struct SymbolTable {
108 structs: HashMap<String, StructInfo>,
110 enums: BTreeMap<String, EnumInfo>,
113 interfaces: HashMap<String, InterfaceInfo>,
115 fns: HashMap<FnKey, FnInfo>,
117}
118
119#[allow(dead_code)]
123struct StructInfo {
124 fields: Vec<(String, QalaType)>,
126 span: Span,
128}
129
130#[allow(dead_code)]
133struct EnumInfo {
134 variants: Vec<(String, Vec<QalaType>)>,
136 span: Span,
138}
139
140#[allow(dead_code)]
143struct InterfaceInfo {
144 methods: Vec<MethodSigInfo>,
146 span: Span,
148}
149
150#[allow(dead_code)]
153struct MethodSigInfo {
154 name: String,
156 params: Vec<QalaType>,
159 ret_ty: QalaType,
161 effect: Option<EffectSet>,
163 span: Span,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Hash)]
172struct FnKey {
173 type_name: Option<String>,
175 name: String,
177}
178
179#[allow(dead_code)]
183struct FnInfo {
184 params: Vec<(String, QalaType, Option<ast::Expr>)>,
188 ret_ty: QalaType,
190 annotated_effect: Option<EffectSet>,
192 inferred_effect: Option<EffectSet>,
195 span: Span,
197 is_method: bool,
199}
200
201#[allow(dead_code)]
206struct LocalInfo {
207 ty: QalaType,
209 span: Span,
211 is_mut: bool,
213 used: bool,
216 is_param: bool,
219}
220
221#[allow(dead_code)]
225struct FnCtx {
226 name: String,
228 type_name: Option<String>,
230 ret_ty: QalaType,
232 annotated_effect: Option<EffectSet>,
234 body_intrinsic: EffectSet,
238 called_fns: Vec<FnKey>,
241 callsites_to_check: Vec<EffectViolationCandidate>,
245}
246
247#[allow(dead_code)]
250#[derive(Clone)]
251struct EffectViolationCandidate {
252 caller_key: FnKey,
254 callee_key: FnKey,
256 call_span: Span,
258}
259
260#[allow(dead_code)]
265struct Checker<'src> {
266 src: &'src str,
269 line_index: LineIndex,
271 symbols: SymbolTable,
273 struct_decl_order: Vec<String>,
277 collected_enum_names: Vec<String>,
281 collected_interface_names: Vec<String>,
283 errors: Vec<QalaError>,
286 warnings: Vec<QalaWarning>,
289 allow: HashMap<usize, BTreeSet<String>>,
292 scopes: Vec<HashMap<String, LocalInfo>>,
295 fn_ctx: Option<FnCtx>,
297 depth: u32,
300 body_records: HashMap<FnKey, BodyEffectRecord>,
304}
305
306impl<'src> Checker<'src> {
307 fn new(src: &'src str) -> Self {
309 Checker {
310 src,
311 line_index: LineIndex::new(src),
312 symbols: SymbolTable::default(),
313 struct_decl_order: Vec::new(),
314 collected_enum_names: Vec::new(),
315 collected_interface_names: Vec::new(),
316 errors: Vec::new(),
317 warnings: Vec::new(),
318 allow: HashMap::new(),
320 scopes: Vec::new(),
321 fn_ctx: None,
322 depth: 0,
323 body_records: HashMap::new(),
324 }
325 }
326}
327
328impl<'src> Checker<'src> {
331 fn preregister_type_names(&mut self, ast: &ast::Ast) {
339 for item in ast {
340 match item {
341 ast::Item::Struct(d) => {
342 self.symbols
343 .structs
344 .entry(d.name.clone())
345 .or_insert(StructInfo {
346 fields: Vec::new(),
347 span: d.span,
348 });
349 }
350 ast::Item::Enum(d) => {
351 self.symbols
352 .enums
353 .entry(d.name.clone())
354 .or_insert(EnumInfo {
355 variants: Vec::new(),
356 span: d.span,
357 });
358 }
359 ast::Item::Interface(d) => {
360 self.symbols
361 .interfaces
362 .entry(d.name.clone())
363 .or_insert(InterfaceInfo {
364 methods: Vec::new(),
365 span: d.span,
366 });
367 }
368 ast::Item::Fn(_) => {}
369 }
370 }
371 }
372
373 fn collect_item(&mut self, item: &ast::Item) {
375 match item {
376 ast::Item::Fn(d) => self.collect_fn(d),
377 ast::Item::Struct(d) => self.collect_struct(d),
378 ast::Item::Enum(d) => self.collect_enum(d),
379 ast::Item::Interface(d) => self.collect_interface(d),
380 }
381 }
382
383 fn collect_struct(&mut self, decl: &ast::StructDecl) {
394 if self.struct_decl_order.contains(&decl.name) {
395 self.errors.push(QalaError::Type {
396 span: decl.span,
397 message: format!("duplicate struct definition `{}`", decl.name),
398 });
399 return;
400 }
401 let mut fields: Vec<(String, QalaType)> = Vec::with_capacity(decl.fields.len());
402 for f in &decl.fields {
403 let ty = self.resolve_type_expr(&f.ty);
404 fields.push((f.name.clone(), ty));
405 }
406 self.symbols.structs.insert(
407 decl.name.clone(),
408 StructInfo {
409 fields,
410 span: decl.span,
411 },
412 );
413 self.struct_decl_order.push(decl.name.clone());
414 }
415
416 fn collect_enum(&mut self, decl: &ast::EnumDecl) {
418 if self.collected_enum_names.contains(&decl.name) {
419 self.errors.push(QalaError::Type {
420 span: decl.span,
421 message: format!("duplicate enum definition `{}`", decl.name),
422 });
423 return;
424 }
425 self.collected_enum_names.push(decl.name.clone());
426 let mut variants: Vec<(String, Vec<QalaType>)> = Vec::with_capacity(decl.variants.len());
427 for v in &decl.variants {
428 let fields: Vec<QalaType> =
429 v.fields.iter().map(|t| self.resolve_type_expr(t)).collect();
430 variants.push((v.name.clone(), fields));
431 }
432 self.symbols.enums.insert(
433 decl.name.clone(),
434 EnumInfo {
435 variants,
436 span: decl.span,
437 },
438 );
439 }
440
441 fn collect_interface(&mut self, decl: &ast::InterfaceDecl) {
443 if self.collected_interface_names.contains(&decl.name) {
444 self.errors.push(QalaError::Type {
445 span: decl.span,
446 message: format!("duplicate interface definition `{}`", decl.name),
447 });
448 return;
449 }
450 self.collected_interface_names.push(decl.name.clone());
451 let methods: Vec<MethodSigInfo> = decl
452 .methods
453 .iter()
454 .map(|m| {
455 let params: Vec<QalaType> = m
462 .params
463 .iter()
464 .map(|p| {
465 if p.is_self {
466 QalaType::Unknown
467 } else if let Some(ty) = &p.ty {
468 self.resolve_type_expr(ty)
469 } else {
470 QalaType::Unknown
471 }
472 })
473 .collect();
474 let ret_ty = m
475 .ret_ty
476 .as_ref()
477 .map(|t| self.resolve_type_expr(t))
478 .unwrap_or(QalaType::Void);
479 let effect = m.effect.as_ref().map(effect_set_from_ast);
480 MethodSigInfo {
481 name: m.name.clone(),
482 params,
483 ret_ty,
484 effect,
485 span: m.span,
486 }
487 })
488 .collect();
489 self.symbols.interfaces.insert(
490 decl.name.clone(),
491 InterfaceInfo {
492 methods,
493 span: decl.span,
494 },
495 );
496 }
497
498 fn collect_fn(&mut self, decl: &ast::FnDecl) {
501 let key = FnKey {
502 type_name: decl.type_name.clone(),
503 name: decl.name.clone(),
504 };
505 if self.symbols.fns.contains_key(&key) {
506 let label = match &decl.type_name {
508 Some(t) => format!("{}.{}", t, decl.name),
509 None => decl.name.clone(),
510 };
511 self.errors.push(QalaError::Type {
512 span: decl.span,
513 message: format!("duplicate function definition `{label}`"),
514 });
515 return;
516 }
517 let mut params: Vec<(String, QalaType, Option<ast::Expr>)> =
518 Vec::with_capacity(decl.params.len());
519 for p in &decl.params {
520 let ty = if p.is_self {
521 match &decl.type_name {
523 Some(t) => QalaType::Named(Symbol(t.clone())),
524 None => QalaType::Unknown,
525 }
526 } else if let Some(t) = &p.ty {
527 self.resolve_type_expr(t)
528 } else {
529 QalaType::Unknown
532 };
533 params.push((p.name.clone(), ty, p.default.clone()));
534 }
535 let ret_ty = decl
536 .ret_ty
537 .as_ref()
538 .map(|t| self.resolve_type_expr(t))
539 .unwrap_or(QalaType::Void);
540 let annotated_effect = decl.effect.as_ref().map(effect_set_from_ast);
541 let is_method = decl.type_name.is_some();
542 self.symbols.fns.insert(
543 key,
544 FnInfo {
545 params,
546 ret_ty,
547 annotated_effect,
548 inferred_effect: None,
549 span: decl.span,
550 is_method,
551 },
552 );
553 }
554
555 fn resolve_type_expr(&mut self, ty: &ast::TypeExpr) -> QalaType {
559 match ty {
560 ast::TypeExpr::Primitive { kind, .. } => QalaType::from_prim_type(kind),
561 ast::TypeExpr::Named { name, span } => {
562 if name == "Result" {
566 self.errors.push(QalaError::Type {
567 span: *span,
568 message: "Result requires two type arguments".to_string(),
569 });
570 return QalaType::Unknown;
571 }
572 if name == "Option" {
573 self.errors.push(QalaError::Type {
574 span: *span,
575 message: "Option requires one type argument".to_string(),
576 });
577 return QalaType::Unknown;
578 }
579 if name == "FileHandle" {
580 return QalaType::FileHandle;
581 }
582 if self.symbols.structs.contains_key(name)
583 || self.symbols.enums.contains_key(name)
584 || self.symbols.interfaces.contains_key(name)
585 {
586 return QalaType::Named(Symbol(name.clone()));
587 }
588 self.errors.push(QalaError::UnknownType {
589 span: *span,
590 name: name.clone(),
591 });
592 QalaType::Unknown
593 }
594 ast::TypeExpr::Array { elem, size, .. } => {
595 QalaType::Array(Box::new(self.resolve_type_expr(elem)), Some(*size as usize))
596 }
597 ast::TypeExpr::DynArray { elem, .. } => {
598 QalaType::Array(Box::new(self.resolve_type_expr(elem)), None)
599 }
600 ast::TypeExpr::Tuple { elems, .. } => {
601 QalaType::Tuple(elems.iter().map(|e| self.resolve_type_expr(e)).collect())
602 }
603 ast::TypeExpr::Fn { params, ret, .. } => QalaType::Function {
604 params: params.iter().map(|p| self.resolve_type_expr(p)).collect(),
605 returns: Box::new(self.resolve_type_expr(ret)),
606 },
607 ast::TypeExpr::Generic { name, args, span } => {
608 if name == "Result" {
609 if args.len() != 2 {
610 self.errors.push(QalaError::Type {
611 span: *span,
612 message: "Result requires two type arguments".to_string(),
613 });
614 return QalaType::Unknown;
615 }
616 let ok = self.resolve_type_expr(&args[0]);
617 let err = self.resolve_type_expr(&args[1]);
618 return QalaType::Result(Box::new(ok), Box::new(err));
619 }
620 if name == "Option" {
621 if args.len() != 1 {
622 self.errors.push(QalaError::Type {
623 span: *span,
624 message: "Option requires one type argument".to_string(),
625 });
626 return QalaType::Unknown;
627 }
628 let inner = self.resolve_type_expr(&args[0]);
629 return QalaType::Option(Box::new(inner));
630 }
631 self.errors.push(QalaError::Type {
632 span: *span,
633 message: format!("unknown generic type `{name}`"),
634 });
635 QalaType::Unknown
636 }
637 }
638 }
639}
640
641fn effect_set_from_ast(e: &ast::Effect) -> EffectSet {
644 match e {
645 ast::Effect::Pure => EffectSet::pure(),
646 ast::Effect::Io => EffectSet::io(),
647 ast::Effect::Alloc => EffectSet::alloc(),
648 ast::Effect::Panic => EffectSet::panic(),
649 }
650}
651
652impl<'src> Checker<'src> {
655 fn detect_recursive_structs(&mut self) {
670 let mut order = self.struct_decl_order.clone();
673 order.sort();
674
675 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
677 for name in &order {
678 let mut targets: BTreeSet<String> = BTreeSet::new();
679 if let Some(info) = self.symbols.structs.get(name) {
680 for (_, ty) in &info.fields {
681 collect_by_value_targets(ty, &mut targets);
682 }
683 }
684 let neighbours: Vec<String> = targets
685 .into_iter()
686 .filter(|n| self.symbols.structs.contains_key(n))
687 .collect();
688 adj.insert(name.clone(), neighbours);
689 }
690
691 let sccs = tarjan_scc(&order, &adj);
693
694 for scc in sccs {
695 let is_cycle = scc.len() > 1
697 || (scc.len() == 1
698 && adj
699 .get(&scc[0])
700 .map(|v| v.contains(&scc[0]))
701 .unwrap_or(false));
702 if !is_cycle {
703 continue;
704 }
705 let head = scc.iter().min().cloned().unwrap_or_else(|| scc[0].clone());
707 let scc_set: BTreeSet<String> = scc.iter().cloned().collect();
711 let mut path: Vec<String> = vec![head.clone()];
712 let mut current = head.clone();
713 for _ in 0..=scc.len() {
716 let next: Option<String> = adj
717 .get(¤t)
718 .and_then(|nbrs| nbrs.iter().filter(|n| scc_set.contains(*n)).min().cloned());
719 let Some(next_name) = next else {
720 break;
721 };
722 path.push(next_name.clone());
723 if next_name == head {
724 break;
725 }
726 current = next_name;
727 }
728 let span = self
730 .symbols
731 .structs
732 .get(&head)
733 .map(|s| s.span)
734 .unwrap_or(Span::new(0, 0));
735 self.errors
736 .push(QalaError::RecursiveStructByValue { span, path });
737 }
738 }
739}
740
741fn collect_by_value_targets(ty: &QalaType, out: &mut BTreeSet<String>) {
747 match ty {
748 QalaType::Named(Symbol(name)) => {
749 out.insert(name.clone());
750 }
751 QalaType::Tuple(elems) => {
752 for e in elems {
753 collect_by_value_targets(e, out);
754 }
755 }
756 QalaType::Array(inner, Some(n)) if *n > 0 => {
757 collect_by_value_targets(inner, out);
758 }
759 _ => {}
763 }
764}
765
766fn tarjan_scc(nodes: &[String], adj: &HashMap<String, Vec<String>>) -> Vec<Vec<String>> {
778 let mut index_of: HashMap<String, u32> = HashMap::new();
780 let mut lowlink: HashMap<String, u32> = HashMap::new();
781 let mut on_stack: HashMap<String, bool> = HashMap::new();
782 let mut stack: Vec<String> = Vec::new();
783 let mut next_index: u32 = 0;
784 let mut sccs: Vec<Vec<String>> = Vec::new();
785
786 struct Frame {
788 node: String,
789 next_child: usize,
790 }
791
792 for root in nodes {
793 if index_of.contains_key(root) {
794 continue;
795 }
796 index_of.insert(root.clone(), next_index);
798 lowlink.insert(root.clone(), next_index);
799 next_index += 1;
800 stack.push(root.clone());
801 on_stack.insert(root.clone(), true);
802
803 let mut call_stack: Vec<Frame> = vec![Frame {
804 node: root.clone(),
805 next_child: 0,
806 }];
807
808 while let Some(frame) = call_stack.last_mut() {
809 let v = frame.node.clone();
810 let empty: Vec<String> = Vec::new();
811 let children = adj.get(&v).unwrap_or(&empty);
812 if frame.next_child < children.len() {
813 let w = children[frame.next_child].clone();
814 frame.next_child += 1;
815 if !index_of.contains_key(&w) {
816 index_of.insert(w.clone(), next_index);
818 lowlink.insert(w.clone(), next_index);
819 next_index += 1;
820 stack.push(w.clone());
821 on_stack.insert(w.clone(), true);
822 call_stack.push(Frame {
823 node: w,
824 next_child: 0,
825 });
826 } else if *on_stack.get(&w).unwrap_or(&false) {
827 let w_idx = *index_of.get(&w).unwrap_or(&0);
829 let v_low = *lowlink.get(&v).unwrap_or(&0);
830 lowlink.insert(v.clone(), v_low.min(w_idx));
831 }
832 } else {
833 let v_idx = *index_of.get(&v).unwrap_or(&0);
835 let v_low = *lowlink.get(&v).unwrap_or(&0);
836 if v_low == v_idx {
837 let mut component: Vec<String> = Vec::new();
838 while let Some(w) = stack.pop() {
843 on_stack.insert(w.clone(), false);
844 let is_v = w == v;
845 component.push(w);
846 if is_v {
847 break;
848 }
849 }
850 sccs.push(component);
851 }
852 call_stack.pop();
853 if let Some(parent_frame) = call_stack.last() {
855 let p = parent_frame.node.clone();
856 let p_low = *lowlink.get(&p).unwrap_or(&0);
857 let v_low = *lowlink.get(&v).unwrap_or(&0);
858 lowlink.insert(p, p_low.min(v_low));
859 }
860 }
861 }
862 }
863 sccs
864}
865
866impl<'src> Checker<'src> {
869 fn check_item(&mut self, item: &ast::Item) -> typed_ast::TypedItem {
878 match item {
879 ast::Item::Fn(d) => self.check_fn_decl(d),
880 ast::Item::Struct(d) => typed_ast::TypedItem::Struct(typed_ast::TypedStructDecl {
881 name: d.name.clone(),
882 fields: d
883 .fields
884 .iter()
885 .map(|f| typed_ast::TypedField {
886 name: f.name.clone(),
887 ty: resolve_type_silent(&f.ty, &self.symbols),
888 span: f.span,
889 })
890 .collect(),
891 span: d.span,
892 }),
893 ast::Item::Enum(d) => typed_ast::TypedItem::Enum(typed_ast::TypedEnumDecl {
894 name: d.name.clone(),
895 variants: d
896 .variants
897 .iter()
898 .map(|v| typed_ast::TypedVariant {
899 name: v.name.clone(),
900 fields: v
901 .fields
902 .iter()
903 .map(|t| resolve_type_silent(t, &self.symbols))
904 .collect(),
905 span: v.span,
906 })
907 .collect(),
908 span: d.span,
909 }),
910 ast::Item::Interface(d) => {
911 typed_ast::TypedItem::Interface(typed_ast::TypedInterfaceDecl {
912 name: d.name.clone(),
913 methods: d
914 .methods
915 .iter()
916 .map(|m| typed_ast::TypedMethodSig {
917 name: m.name.clone(),
918 params: m
919 .params
920 .iter()
921 .map(|p| typed_ast::TypedParam {
922 is_self: p.is_self,
923 name: p.name.clone(),
924 ty: if p.is_self {
925 QalaType::Unknown
926 } else if let Some(t) = &p.ty {
927 resolve_type_silent(t, &self.symbols)
928 } else {
929 QalaType::Unknown
930 },
931 default: None,
932 span: p.span,
933 })
934 .collect(),
935 ret_ty: m
936 .ret_ty
937 .as_ref()
938 .map(|t| resolve_type_silent(t, &self.symbols))
939 .unwrap_or(QalaType::Void),
940 effect: m
941 .effect
942 .as_ref()
943 .map(effect_set_from_ast)
944 .unwrap_or(EffectSet::pure()),
945 span: m.span,
946 })
947 .collect(),
948 span: d.span,
949 })
950 }
951 }
952 }
953
954 fn is_resource_returning(name: &str) -> bool {
958 matches!(name, "open")
959 }
960
961 fn extract_closed_handle(expr: &ast::Expr) -> Option<String> {
965 match expr {
966 ast::Expr::Call { callee, args, .. } => {
967 if let ast::Expr::Ident { name, .. } = callee.as_ref()
968 && name == "close"
969 && args.len() == 1
970 && let ast::Expr::Ident { name: h, .. } = &args[0]
971 {
972 return Some(h.clone());
973 }
974 None
975 }
976 ast::Expr::MethodCall {
977 receiver,
978 name,
979 args,
980 ..
981 } => {
982 if name == "close"
983 && args.is_empty()
984 && let ast::Expr::Ident { name: h, .. } = receiver.as_ref()
985 {
986 return Some(h.clone());
987 }
988 None
989 }
990 _ => None,
991 }
992 }
993
994 fn check_unmatched_defer(&mut self, block: &ast::Block) {
997 let mut handle_bindings: Vec<(String, Span)> = Vec::new();
1001 for stmt in &block.stmts {
1002 if let ast::Stmt::Let {
1003 name, init, span, ..
1004 } = stmt
1005 && let ast::Expr::Call { callee, .. } = init
1006 && let ast::Expr::Ident { name: cname, .. } = callee.as_ref()
1007 && Self::is_resource_returning(cname)
1008 {
1009 handle_bindings.push((name.clone(), *span));
1010 }
1011 }
1012 let mut closed: BTreeSet<String> = BTreeSet::new();
1014 for stmt in &block.stmts {
1015 if let ast::Stmt::Defer { expr, .. } = stmt
1016 && let Some(h) = Self::extract_closed_handle(expr)
1017 {
1018 closed.insert(h);
1019 }
1020 }
1021 for (name, span) in &handle_bindings {
1023 if !closed.contains(name) {
1024 let w = QalaWarning {
1025 category: "unmatched_defer".to_string(),
1026 message: format!(
1027 "resource `{name}` is acquired without a matching `defer close`"
1028 ),
1029 span: *span,
1030 note: None,
1031 };
1032 self.emit_warning(w);
1033 }
1034 }
1035 for stmt in &block.stmts {
1037 self.check_unmatched_defer_stmt(stmt);
1038 }
1039 if let Some(boxed) = &block.value
1041 && let ast::Expr::Block { block: inner, .. } = boxed.as_ref()
1042 {
1043 self.check_unmatched_defer(inner);
1044 }
1045 }
1046
1047 fn check_unmatched_defer_stmt(&mut self, stmt: &ast::Stmt) {
1051 match stmt {
1052 ast::Stmt::If {
1053 then_block,
1054 else_branch,
1055 ..
1056 } => {
1057 self.check_unmatched_defer(then_block);
1058 match else_branch {
1059 Some(ast::ElseBranch::Block(b)) => self.check_unmatched_defer(b),
1060 Some(ast::ElseBranch::If(boxed)) => {
1061 self.check_unmatched_defer_stmt(boxed);
1064 }
1065 None => {}
1066 }
1067 }
1068 ast::Stmt::While { body, .. } => self.check_unmatched_defer(body),
1069 ast::Stmt::For { body, .. } => self.check_unmatched_defer(body),
1070 _ => {}
1071 }
1072 }
1073
1074 fn check_fn_decl(&mut self, d: &ast::FnDecl) -> typed_ast::TypedItem {
1077 let ret_ty = d
1078 .ret_ty
1079 .as_ref()
1080 .map(|t| resolve_type_silent(t, &self.symbols))
1081 .unwrap_or(QalaType::Void);
1082 let annotated_effect = d.effect.as_ref().map(effect_set_from_ast);
1083 let mut typed_params: Vec<typed_ast::TypedParam> = Vec::with_capacity(d.params.len());
1086 let mut param_locals: Vec<(String, LocalInfo)> = Vec::with_capacity(d.params.len());
1087 for p in &d.params {
1088 let ty = if p.is_self {
1089 match &d.type_name {
1090 Some(t) => QalaType::Named(Symbol(t.clone())),
1091 None => QalaType::Unknown,
1092 }
1093 } else if let Some(t) = &p.ty {
1094 resolve_type_silent(t, &self.symbols)
1095 } else {
1096 QalaType::Unknown
1097 };
1098 typed_params.push(typed_ast::TypedParam {
1099 is_self: p.is_self,
1100 name: p.name.clone(),
1101 ty: ty.clone(),
1102 default: None,
1103 span: p.span,
1104 });
1105 param_locals.push((
1106 p.name.clone(),
1107 LocalInfo {
1108 ty,
1109 span: p.span,
1110 is_mut: false,
1111 used: false,
1112 is_param: true,
1113 },
1114 ));
1115 }
1116 self.fn_ctx = Some(FnCtx {
1118 name: d.name.clone(),
1119 type_name: d.type_name.clone(),
1120 ret_ty: ret_ty.clone(),
1121 annotated_effect,
1122 body_intrinsic: EffectSet::pure(),
1123 called_fns: Vec::new(),
1124 callsites_to_check: Vec::new(),
1125 });
1126 let mut scope: HashMap<String, LocalInfo> = HashMap::new();
1127 for (name, info) in param_locals {
1128 scope.insert(name, info);
1129 }
1130 self.scopes.push(scope);
1131
1132 let body = self.check_block(&d.body, Some(&ret_ty));
1134 self.check_unmatched_defer(&d.body);
1137
1138 if !ret_ty.types_match(&QalaType::Void) && body.value.is_none() {
1144 let ends_in_return = d
1145 .body
1146 .stmts
1147 .last()
1148 .map(|s| matches!(s, ast::Stmt::Return { .. }))
1149 .unwrap_or(false);
1150 if !ends_in_return {
1155 let span = Span::new(d.body.span.end().saturating_sub(1), 1);
1156 self.errors.push(QalaError::MissingReturn {
1157 span,
1158 fn_name: d.name.clone(),
1159 expected: ret_ty.display(),
1160 });
1161 }
1162 }
1163
1164 if let Some(scope) = self.scopes.pop() {
1166 for (name, info) in scope {
1167 if !info.is_param && !info.used && !name.starts_with('_') {
1168 let w = QalaWarning {
1169 category: "unused_var".to_string(),
1170 message: format!("unused variable `{name}`"),
1171 span: info.span,
1172 note: None,
1173 };
1174 self.emit_warning(w);
1175 }
1176 }
1177 }
1178
1179 let ctx = self.fn_ctx.take();
1182 if let Some(ctx) = ctx {
1183 let key = FnKey {
1185 type_name: d.type_name.clone(),
1186 name: d.name.clone(),
1187 };
1188 self.body_records.insert(
1189 key,
1190 BodyEffectRecord {
1191 intrinsic: ctx.body_intrinsic,
1192 called: ctx.called_fns,
1193 callsites_to_check: ctx.callsites_to_check,
1194 },
1195 );
1196 }
1197
1198 typed_ast::TypedItem::Fn(typed_ast::TypedFnDecl {
1199 type_name: d.type_name.clone(),
1200 name: d.name.clone(),
1201 params: typed_params,
1202 ret_ty,
1203 effect: annotated_effect.unwrap_or(EffectSet::pure()),
1204 body,
1205 span: d.span,
1206 })
1207 }
1208
1209 fn check_block(
1213 &mut self,
1214 block: &ast::Block,
1215 expected: Option<&QalaType>,
1216 ) -> typed_ast::TypedBlock {
1217 let pushed_scope = !self.scopes.is_empty();
1221 if pushed_scope {
1222 self.scopes.push(HashMap::new());
1223 }
1224 let mut typed_stmts: Vec<typed_ast::TypedStmt> = Vec::with_capacity(block.stmts.len());
1225 let mut terminator: Option<Span> = None;
1226 let mut warned_unreachable = false;
1227 for stmt in &block.stmts {
1228 if terminator.is_some() && !warned_unreachable {
1229 let w = QalaWarning {
1230 category: "unreachable_code".to_string(),
1231 message: "unreachable statement after `return`, `break`, or `continue`"
1232 .to_string(),
1233 span: stmt.span(),
1234 note: None,
1235 };
1236 self.emit_warning(w);
1237 warned_unreachable = true;
1238 }
1241 let typed_stmt = self.check_stmt(stmt);
1242 let is_terminator = matches!(
1243 stmt,
1244 ast::Stmt::Return { .. } | ast::Stmt::Break { .. } | ast::Stmt::Continue { .. }
1245 );
1246 if is_terminator && terminator.is_none() {
1247 terminator = Some(stmt.span());
1248 }
1249 typed_stmts.push(typed_stmt);
1250 }
1251 let (typed_value, value_ty) = match &block.value {
1252 Some(v) => {
1253 let typed = match expected {
1254 Some(ty) => self.check_expr(v, ty),
1255 None => self.infer_expr(v),
1256 };
1257 let ty = typed.ty().clone();
1258 (Some(Box::new(typed)), ty)
1259 }
1260 None => (None, QalaType::Void),
1261 };
1262 if pushed_scope && let Some(scope) = self.scopes.pop() {
1264 for (name, info) in scope {
1265 if !info.is_param && !info.used && !name.starts_with('_') {
1266 let w = QalaWarning {
1267 category: "unused_var".to_string(),
1268 message: format!("unused variable `{name}`"),
1269 span: info.span,
1270 note: None,
1271 };
1272 self.emit_warning(w);
1273 }
1274 }
1275 }
1276 typed_ast::TypedBlock {
1277 stmts: typed_stmts,
1278 value: typed_value,
1279 ty: value_ty,
1280 span: block.span,
1281 }
1282 }
1283
1284 fn check_stmt(&mut self, stmt: &ast::Stmt) -> typed_ast::TypedStmt {
1286 match stmt {
1287 ast::Stmt::Let {
1288 is_mut,
1289 name,
1290 ty,
1291 init,
1292 span,
1293 } => {
1294 let (typed_init, decl_ty) = match ty {
1295 Some(t) => {
1296 let expected = resolve_type_silent(t, &self.symbols);
1297 let typed_init = self.check_expr(init, &expected);
1298 if types_strictly_equal(typed_init.ty(), &expected) {
1302 let w = QalaWarning {
1303 category: "redundant_annotation".to_string(),
1304 message: format!(
1305 "redundant type annotation: `{}` matches the inferred type",
1306 expected.display()
1307 ),
1308 span: t.span(),
1309 note: None,
1310 };
1311 self.emit_warning(w);
1312 }
1313 if let QalaType::Named(Symbol(iname)) = &expected
1318 && self.symbols.interfaces.contains_key(iname)
1319 && let QalaType::Named(Symbol(_)) = typed_init.ty()
1320 {
1321 let init_ty = typed_init.ty().clone();
1322 self.check_satisfies(&init_ty, iname, *span);
1323 }
1324 (typed_init, expected)
1325 }
1326 None => {
1327 let typed_init = self.infer_expr(init);
1328 let ty = typed_init.ty().clone();
1329 (typed_init, ty)
1330 }
1331 };
1332 let topmost = self.scopes.len().saturating_sub(1);
1335 let mut prior_span: Option<Span> = None;
1336 for (idx, scope) in self.scopes.iter().enumerate() {
1337 if idx == topmost {
1338 break;
1339 }
1340 if let Some(prior) = scope.get(name) {
1341 prior_span = Some(prior.span);
1342 break;
1343 }
1344 }
1345 if let Some(prior) = prior_span {
1346 let (l, c) = self.line_index.location(self.src, prior.start as usize);
1347 let w = QalaWarning {
1348 category: "shadowed_var".to_string(),
1349 message: format!("`{name}` shadows a binding from an outer scope"),
1350 span: *span,
1351 note: Some(format!("the prior binding is at line {l}:{c}")),
1352 };
1353 self.emit_warning(w);
1354 }
1355 if let Some(scope) = self.scopes.last_mut() {
1357 scope.insert(
1358 name.clone(),
1359 LocalInfo {
1360 ty: decl_ty.clone(),
1361 span: *span,
1362 is_mut: *is_mut,
1363 used: false,
1364 is_param: false,
1365 },
1366 );
1367 }
1368 typed_ast::TypedStmt::Let {
1369 is_mut: *is_mut,
1370 name: name.clone(),
1371 ty: decl_ty,
1372 init: typed_init,
1373 span: *span,
1374 }
1375 }
1376 ast::Stmt::If {
1377 cond,
1378 then_block,
1379 else_branch,
1380 span,
1381 } => {
1382 let typed_cond = self.check_expr(cond, &QalaType::Bool);
1383 let typed_then = self.check_block(then_block, None);
1384 let typed_else = match else_branch {
1385 Some(ast::ElseBranch::Block(b)) => {
1386 Some(typed_ast::TypedElseBranch::Block(self.check_block(b, None)))
1387 }
1388 Some(ast::ElseBranch::If(b)) => {
1389 let inner = self.check_stmt(b);
1390 Some(typed_ast::TypedElseBranch::If(Box::new(inner)))
1391 }
1392 None => None,
1393 };
1394 typed_ast::TypedStmt::If {
1395 cond: typed_cond,
1396 then_block: typed_then,
1397 else_branch: typed_else,
1398 span: *span,
1399 }
1400 }
1401 ast::Stmt::While { cond, body, span } => {
1402 let typed_cond = self.check_expr(cond, &QalaType::Bool);
1403 let typed_body = self.check_block(body, None);
1404 typed_ast::TypedStmt::While {
1405 cond: typed_cond,
1406 body: typed_body,
1407 span: *span,
1408 }
1409 }
1410 ast::Stmt::For {
1411 var,
1412 iter,
1413 body,
1414 span,
1415 } => {
1416 let typed_iter = self.infer_expr(iter);
1417 let var_ty = match typed_iter.ty() {
1421 QalaType::Array(elem, _) => (**elem).clone(),
1422 QalaType::Unknown => QalaType::Unknown,
1423 _ => {
1424 self.errors.push(QalaError::Type {
1425 span: iter.span(),
1426 message: "for loop expects an array or range".to_string(),
1427 });
1428 QalaType::Unknown
1429 }
1430 };
1431 let mut scope: HashMap<String, LocalInfo> = HashMap::new();
1433 scope.insert(
1434 var.clone(),
1435 LocalInfo {
1436 ty: var_ty.clone(),
1437 span: *span,
1438 is_mut: false,
1439 used: false,
1442 is_param: true,
1443 },
1444 );
1445 self.scopes.push(scope);
1446 let typed_body = self.check_block(body, None);
1447 self.scopes.pop();
1448 typed_ast::TypedStmt::For {
1449 var: var.clone(),
1450 var_ty,
1451 iter: typed_iter,
1452 body: typed_body,
1453 span: *span,
1454 }
1455 }
1456 ast::Stmt::Return { value, span } => {
1457 let typed_value = match value {
1458 Some(v) => {
1459 let expected = self.fn_ctx.as_ref().map(|c| c.ret_ty.clone());
1460 let typed = match expected {
1461 Some(ty) => self.check_expr(v, &ty),
1462 None => self.infer_expr(v),
1463 };
1464 Some(typed)
1465 }
1466 None => {
1467 if let Some(ctx) = &self.fn_ctx
1469 && !ctx.ret_ty.types_match(&QalaType::Void)
1470 {
1471 self.errors.push(QalaError::TypeMismatch {
1472 span: *span,
1473 expected: ctx.ret_ty.display(),
1474 found: "void".to_string(),
1475 });
1476 }
1477 None
1478 }
1479 };
1480 typed_ast::TypedStmt::Return {
1481 value: typed_value,
1482 span: *span,
1483 }
1484 }
1485 ast::Stmt::Break { span } => typed_ast::TypedStmt::Break { span: *span },
1486 ast::Stmt::Continue { span } => typed_ast::TypedStmt::Continue { span: *span },
1487 ast::Stmt::Defer { expr, span } => {
1488 let typed_expr = self.infer_expr(expr);
1489 typed_ast::TypedStmt::Defer {
1490 expr: typed_expr,
1491 span: *span,
1492 }
1493 }
1494 ast::Stmt::Expr { expr, span } => {
1495 let typed_expr = self.infer_expr(expr);
1496 typed_ast::TypedStmt::Expr {
1497 expr: typed_expr,
1498 span: *span,
1499 }
1500 }
1501 }
1502 }
1503
1504 fn lookup(&mut self, name: &str) -> Option<QalaType> {
1509 for scope in self.scopes.iter_mut().rev() {
1510 if let Some(info) = scope.get_mut(name) {
1511 info.used = true;
1512 return Some(info.ty.clone());
1513 }
1514 }
1515 let key = FnKey {
1517 type_name: None,
1518 name: name.to_string(),
1519 };
1520 if let Some(info) = self.symbols.fns.get(&key) {
1521 let params: Vec<QalaType> = info.params.iter().map(|(_, ty, _)| ty.clone()).collect();
1522 return Some(QalaType::Function {
1523 params,
1524 returns: Box::new(info.ret_ty.clone()),
1525 });
1526 }
1527 let stdlib = stdlib_signatures();
1529 if let Some(entry) = stdlib
1530 .iter()
1531 .find(|e| e.type_name.is_none() && e.name == name)
1532 {
1533 return Some(QalaType::Function {
1534 params: entry.params.clone(),
1535 returns: Box::new(entry.ret_ty.clone()),
1536 });
1537 }
1538 None
1539 }
1540
1541 fn infer_expr(&mut self, expr: &ast::Expr) -> typed_ast::TypedExpr {
1549 self.depth = self.depth.saturating_add(1);
1550 if self.depth > MAX_DEPTH {
1551 self.errors.push(QalaError::Type {
1552 span: expr.span(),
1553 message: "expression nests too deeply".to_string(),
1554 });
1555 self.depth = self.depth.saturating_sub(1);
1556 return typed_ast::TypedExpr::Int {
1557 value: 0,
1558 ty: QalaType::Unknown,
1559 span: expr.span(),
1560 };
1561 }
1562 let out = self.infer_expr_inner(expr);
1563 self.depth = self.depth.saturating_sub(1);
1564 out
1565 }
1566
1567 fn infer_expr_inner(&mut self, expr: &ast::Expr) -> typed_ast::TypedExpr {
1570 match expr {
1571 ast::Expr::Int { value, span } => typed_ast::TypedExpr::Int {
1572 value: *value,
1573 ty: QalaType::I64,
1574 span: *span,
1575 },
1576 ast::Expr::Float { value, span } => typed_ast::TypedExpr::Float {
1577 value: *value,
1578 ty: QalaType::F64,
1579 span: *span,
1580 },
1581 ast::Expr::Byte { value, span } => typed_ast::TypedExpr::Byte {
1582 value: *value,
1583 ty: QalaType::Byte,
1584 span: *span,
1585 },
1586 ast::Expr::Str { value, span } => typed_ast::TypedExpr::Str {
1587 value: value.clone(),
1588 ty: QalaType::Str,
1589 span: *span,
1590 },
1591 ast::Expr::Bool { value, span } => typed_ast::TypedExpr::Bool {
1592 value: *value,
1593 ty: QalaType::Bool,
1594 span: *span,
1595 },
1596 ast::Expr::Ident { name, span } => {
1597 if let Some(ty) = self.lookup(name) {
1603 return typed_ast::TypedExpr::Ident {
1604 name: name.clone(),
1605 ty,
1606 span: *span,
1607 };
1608 }
1609 let mut variant_match: Option<String> = None;
1611 for (enum_name, info) in &self.symbols.enums {
1612 if info
1613 .variants
1614 .iter()
1615 .any(|(vn, fields)| vn == name && fields.is_empty())
1616 {
1617 variant_match = Some(enum_name.clone());
1618 break;
1619 }
1620 }
1621 if let Some(enum_name) = variant_match {
1622 return typed_ast::TypedExpr::Ident {
1623 name: name.clone(),
1624 ty: QalaType::Named(Symbol(enum_name)),
1625 span: *span,
1626 };
1627 }
1628 self.errors.push(QalaError::UndefinedName {
1629 span: *span,
1630 name: name.clone(),
1631 });
1632 typed_ast::TypedExpr::Ident {
1633 name: name.clone(),
1634 ty: QalaType::Unknown,
1635 span: *span,
1636 }
1637 }
1638 ast::Expr::Paren { inner, span } => {
1639 let typed_inner = self.infer_expr(inner);
1640 let ty = typed_inner.ty().clone();
1641 typed_ast::TypedExpr::Paren {
1642 inner: Box::new(typed_inner),
1643 ty,
1644 span: *span,
1645 }
1646 }
1647 ast::Expr::Tuple { elems, span } => {
1648 let typed_elems: Vec<typed_ast::TypedExpr> =
1649 elems.iter().map(|e| self.infer_expr(e)).collect();
1650 let ty = QalaType::Tuple(typed_elems.iter().map(|e| e.ty().clone()).collect());
1651 typed_ast::TypedExpr::Tuple {
1652 elems: typed_elems,
1653 ty,
1654 span: *span,
1655 }
1656 }
1657 ast::Expr::ArrayLit { elems, span } => {
1658 if elems.is_empty() {
1659 self.errors.push(QalaError::Type {
1660 span: *span,
1661 message: "cannot infer element type of empty array".to_string(),
1662 });
1663 return typed_ast::TypedExpr::ArrayLit {
1664 elems: Vec::new(),
1665 ty: QalaType::Array(Box::new(QalaType::Unknown), Some(0)),
1666 span: *span,
1667 };
1668 }
1669 let first = self.infer_expr(&elems[0]);
1670 let elem_ty = first.ty().clone();
1671 let mut typed_elems = vec![first];
1672 for e in &elems[1..] {
1673 let typed = self.check_expr(e, &elem_ty);
1674 typed_elems.push(typed);
1675 }
1676 let len = typed_elems.len();
1677 typed_ast::TypedExpr::ArrayLit {
1678 elems: typed_elems,
1679 ty: QalaType::Array(Box::new(elem_ty), Some(len)),
1680 span: *span,
1681 }
1682 }
1683 ast::Expr::ArrayRepeat { value, count, span } => {
1684 let typed_value = self.infer_expr(value);
1685 let typed_count = self.check_expr(count, &QalaType::I64);
1686 let elem_ty = typed_value.ty().clone();
1687 typed_ast::TypedExpr::ArrayRepeat {
1688 value: Box::new(typed_value),
1689 count: Box::new(typed_count),
1690 ty: QalaType::Array(Box::new(elem_ty), None),
1691 span: *span,
1692 }
1693 }
1694 ast::Expr::StructLit { name, fields, span } => {
1695 self.check_struct_lit(name, fields, *span)
1697 }
1698 ast::Expr::FieldAccess { obj, name, span } => {
1699 let typed_obj = self.infer_expr(obj);
1700 let obj_ty = typed_obj.ty().clone();
1701 let field_ty = match &obj_ty {
1702 QalaType::Named(Symbol(s)) => {
1703 if let Some(info) = self.symbols.structs.get(s) {
1704 match info.fields.iter().find(|(fn_, _)| fn_ == name) {
1705 Some((_, t)) => t.clone(),
1706 None => {
1707 self.errors.push(QalaError::Type {
1708 span: *span,
1709 message: format!(
1710 "no field `{name}` on type `{}`",
1711 obj_ty.display()
1712 ),
1713 });
1714 QalaType::Unknown
1715 }
1716 }
1717 } else {
1718 self.errors.push(QalaError::Type {
1719 span: *span,
1720 message: format!(
1721 "no field `{name}` on type `{}`",
1722 obj_ty.display()
1723 ),
1724 });
1725 QalaType::Unknown
1726 }
1727 }
1728 QalaType::Unknown => QalaType::Unknown,
1729 _ => {
1730 self.errors.push(QalaError::Type {
1731 span: *span,
1732 message: format!("no field `{name}` on type `{}`", obj_ty.display()),
1733 });
1734 QalaType::Unknown
1735 }
1736 };
1737 typed_ast::TypedExpr::FieldAccess {
1738 obj: Box::new(typed_obj),
1739 name: name.clone(),
1740 ty: field_ty,
1741 span: *span,
1742 }
1743 }
1744 ast::Expr::MethodCall {
1745 receiver,
1746 name,
1747 args,
1748 span,
1749 } => self.check_method_call(receiver, name, args, *span),
1750 ast::Expr::Call { callee, args, span } => self.check_call(callee, args, *span),
1751 ast::Expr::Index { obj, index, span } => {
1752 let typed_obj = self.infer_expr(obj);
1753 let typed_index = self.check_expr(index, &QalaType::I64);
1754 let elem_ty = match typed_obj.ty() {
1755 QalaType::Array(elem, _) => (**elem).clone(),
1756 QalaType::Unknown => QalaType::Unknown,
1757 other => {
1758 self.errors.push(QalaError::Type {
1759 span: *span,
1760 message: format!("cannot index `{}`", other.display()),
1761 });
1762 QalaType::Unknown
1763 }
1764 };
1765 typed_ast::TypedExpr::Index {
1766 obj: Box::new(typed_obj),
1767 index: Box::new(typed_index),
1768 ty: elem_ty,
1769 span: *span,
1770 }
1771 }
1772 ast::Expr::Try { expr: inner, span } => {
1773 let typed_inner = self.infer_expr(inner);
1774 let (success_ty, ok_for_fn) = match typed_inner.ty() {
1775 QalaType::Result(ok, _) => ((**ok).clone(), true),
1776 QalaType::Option(t) => ((**t).clone(), true),
1777 QalaType::Unknown => (QalaType::Unknown, true),
1778 _ => {
1779 self.errors.push(QalaError::Type {
1780 span: *span,
1781 message: format!(
1782 "`?` operand must be Result or Option, found `{}`",
1783 typed_inner.ty().display()
1784 ),
1785 });
1786 (QalaType::Unknown, false)
1787 }
1788 };
1789 if ok_for_fn && let Some(ctx) = &self.fn_ctx {
1792 let ok = matches!(
1793 &ctx.ret_ty,
1794 QalaType::Result(_, _) | QalaType::Option(_) | QalaType::Unknown
1795 );
1796 if !ok {
1797 self.errors.push(QalaError::RedundantQuestionOperator {
1798 span: *span,
1799 message: format!(
1800 "`?` outside a Result-returning or Option-returning function (return type is `{}`); change the return type to `Result<_, _>` or `Option<_>`",
1801 ctx.ret_ty.display()
1802 ),
1803 });
1804 }
1805 }
1806 typed_ast::TypedExpr::Try {
1807 expr: Box::new(typed_inner),
1808 ty: success_ty,
1809 span: *span,
1810 }
1811 }
1812 ast::Expr::Unary { op, operand, span } => {
1813 use ast::UnaryOp;
1814 let typed_operand = self.infer_expr(operand);
1815 let op_ty = typed_operand.ty().clone();
1816 let ty = match op {
1817 UnaryOp::Not => {
1818 if !op_ty.types_match(&QalaType::Bool) {
1819 self.errors.push(QalaError::TypeMismatch {
1820 span: operand.span(),
1821 expected: "bool".to_string(),
1822 found: op_ty.display(),
1823 });
1824 }
1825 QalaType::Bool
1826 }
1827 UnaryOp::Neg => {
1828 if op_ty.types_match(&QalaType::I64) {
1829 QalaType::I64
1830 } else if op_ty.types_match(&QalaType::F64) {
1831 QalaType::F64
1832 } else if matches!(op_ty, QalaType::Unknown) {
1833 QalaType::Unknown
1834 } else {
1835 self.errors.push(QalaError::TypeMismatch {
1836 span: operand.span(),
1837 expected: "i64 or f64".to_string(),
1838 found: op_ty.display(),
1839 });
1840 QalaType::Unknown
1841 }
1842 }
1843 };
1844 typed_ast::TypedExpr::Unary {
1845 op: op.clone(),
1846 operand: Box::new(typed_operand),
1847 ty,
1848 span: *span,
1849 }
1850 }
1851 ast::Expr::Binary { op, lhs, rhs, span } => self.check_binary(op, lhs, rhs, *span),
1852 ast::Expr::Range {
1853 start,
1854 end,
1855 inclusive,
1856 span,
1857 } => {
1858 let typed_start = start.as_ref().map(|e| self.check_expr(e, &QalaType::I64));
1859 let typed_end = end.as_ref().map(|e| self.check_expr(e, &QalaType::I64));
1860 typed_ast::TypedExpr::Range {
1862 start: typed_start.map(Box::new),
1863 end: typed_end.map(Box::new),
1864 inclusive: *inclusive,
1865 ty: QalaType::Array(Box::new(QalaType::I64), None),
1866 span: *span,
1867 }
1868 }
1869 ast::Expr::Pipeline { lhs, call, span } => {
1870 let typed_lhs = self.infer_expr(lhs);
1875 let lhs_ty = typed_lhs.ty().clone();
1876 let (typed_call, result_ty) = self.check_pipeline_call(call, &lhs_ty);
1879 typed_ast::TypedExpr::Pipeline {
1880 lhs: Box::new(typed_lhs),
1881 call: Box::new(typed_call),
1882 ty: result_ty,
1883 span: *span,
1884 }
1885 }
1886 ast::Expr::Comptime { body, span } => {
1887 let typed_body = self.infer_expr(body);
1888 let ty = typed_body.ty().clone();
1889 typed_ast::TypedExpr::Comptime {
1890 body: Box::new(typed_body),
1891 ty,
1892 span: *span,
1893 }
1894 }
1895 ast::Expr::Block { block, span } => {
1896 let typed_block = self.check_block(block, None);
1897 let ty = typed_block.ty.clone();
1898 typed_ast::TypedExpr::Block {
1899 block: typed_block,
1900 ty,
1901 span: *span,
1902 }
1903 }
1904 ast::Expr::Match {
1905 scrutinee,
1906 arms,
1907 span,
1908 } => self.check_match(scrutinee, arms, *span),
1909 ast::Expr::OrElse {
1910 expr: e,
1911 fallback,
1912 span,
1913 } => {
1914 let typed_e = self.infer_expr(e);
1915 let success_ty = match typed_e.ty() {
1916 QalaType::Result(ok, _) => (**ok).clone(),
1917 QalaType::Option(t) => (**t).clone(),
1918 QalaType::Unknown => QalaType::Unknown,
1919 other => {
1920 self.errors.push(QalaError::Type {
1921 span: e.span(),
1922 message: format!(
1923 "`or` left operand must be Result or Option, found `{}`",
1924 other.display()
1925 ),
1926 });
1927 QalaType::Unknown
1928 }
1929 };
1930 let typed_fallback = self.check_expr(fallback, &success_ty);
1931 typed_ast::TypedExpr::OrElse {
1932 expr: Box::new(typed_e),
1933 fallback: Box::new(typed_fallback),
1934 ty: success_ty,
1935 span: *span,
1936 }
1937 }
1938 ast::Expr::Interpolation { parts, span } => {
1939 let typed_parts: Vec<typed_ast::TypedInterpPart> = parts
1940 .iter()
1941 .map(|p| match p {
1942 ast::InterpPart::Literal(s) => {
1943 typed_ast::TypedInterpPart::Literal(s.clone())
1944 }
1945 ast::InterpPart::Expr(e) => {
1946 typed_ast::TypedInterpPart::Expr(self.infer_expr(e))
1947 }
1948 })
1949 .collect();
1950 typed_ast::TypedExpr::Interpolation {
1951 parts: typed_parts,
1952 ty: QalaType::Str,
1953 span: *span,
1954 }
1955 }
1956 }
1957 }
1958
1959 fn check_expr(&mut self, expr: &ast::Expr, expected: &QalaType) -> typed_ast::TypedExpr {
1963 let typed = self.infer_expr(expr);
1964 if !typed.ty().types_match(expected) {
1965 self.errors.push(QalaError::TypeMismatch {
1966 span: typed.span(),
1967 expected: expected.display(),
1968 found: typed.ty().display(),
1969 });
1970 }
1971 typed
1972 }
1973
1974 fn check_struct_lit(
1979 &mut self,
1980 name: &str,
1981 fields: &[ast::FieldInit],
1982 span: Span,
1983 ) -> typed_ast::TypedExpr {
1984 let declared: Option<Vec<(String, QalaType)>> =
1987 self.symbols.structs.get(name).map(|s| s.fields.clone());
1988 let Some(declared) = declared else {
1989 self.errors.push(QalaError::UndefinedName {
1990 span,
1991 name: name.to_string(),
1992 });
1993 return typed_ast::TypedExpr::StructLit {
1994 name: name.to_string(),
1995 fields: Vec::new(),
1996 ty: QalaType::Unknown,
1997 span,
1998 };
1999 };
2000 let mut typed_fields: Vec<typed_ast::TypedFieldInit> = Vec::new();
2001 let provided_names: BTreeSet<String> = fields.iter().map(|f| f.name.clone()).collect();
2003 let missing: Vec<String> = declared
2004 .iter()
2005 .filter(|(dn, _)| !provided_names.contains(dn))
2006 .map(|(dn, _)| dn.clone())
2007 .collect();
2008 if !missing.is_empty() {
2009 self.errors.push(QalaError::Type {
2010 span,
2011 message: format!(
2012 "missing field(s) in struct literal `{name}`: {}",
2013 missing.join(", ")
2014 ),
2015 });
2016 }
2017 for fi in fields {
2018 let decl_ty: Option<QalaType> = declared
2019 .iter()
2020 .find(|(dn, _)| dn == &fi.name)
2021 .map(|(_, t)| t.clone());
2022 match decl_ty {
2023 Some(t) => {
2024 let typed_value = self.check_expr(&fi.value, &t);
2025 typed_fields.push(typed_ast::TypedFieldInit {
2026 name: fi.name.clone(),
2027 value: typed_value,
2028 span: fi.span,
2029 });
2030 }
2031 None => {
2032 self.errors.push(QalaError::Type {
2033 span: fi.span,
2034 message: format!("no field `{}` on struct `{name}`", fi.name),
2035 });
2036 let typed_value = self.infer_expr(&fi.value);
2038 typed_fields.push(typed_ast::TypedFieldInit {
2039 name: fi.name.clone(),
2040 value: typed_value,
2041 span: fi.span,
2042 });
2043 }
2044 }
2045 }
2046 typed_ast::TypedExpr::StructLit {
2047 name: name.to_string(),
2048 fields: typed_fields,
2049 ty: QalaType::Named(Symbol(name.to_string())),
2050 span,
2051 }
2052 }
2053
2054 fn check_method_call(
2058 &mut self,
2059 receiver: &ast::Expr,
2060 name: &str,
2061 args: &[ast::Expr],
2062 span: Span,
2063 ) -> typed_ast::TypedExpr {
2064 let typed_receiver = self.infer_expr(receiver);
2065 let receiver_ty = typed_receiver.ty().clone();
2066 let resolved: Option<(Vec<QalaType>, QalaType, FnKey)> = (|| {
2068 let type_name = match &receiver_ty {
2070 QalaType::Named(Symbol(s)) => Some(s.clone()),
2071 QalaType::FileHandle => Some("FileHandle".to_string()),
2072 _ => None,
2073 };
2074 if let Some(tn) = type_name {
2075 let key = FnKey {
2076 type_name: Some(tn.clone()),
2077 name: name.to_string(),
2078 };
2079 if let Some(info) = self.symbols.fns.get(&key) {
2080 let params: Vec<QalaType> = info
2082 .params
2083 .iter()
2084 .skip(
2085 if info
2086 .params
2087 .first()
2088 .map(|(n, _, _)| n == "self")
2089 .unwrap_or(false)
2090 {
2091 1
2092 } else {
2093 0
2094 },
2095 )
2096 .map(|(_, t, _)| t.clone())
2097 .collect();
2098 return Some((params, info.ret_ty.clone(), key));
2099 }
2100 for entry in stdlib_signatures().iter() {
2102 if entry.type_name.as_deref() == Some(tn.as_str()) && entry.name == name {
2103 let params: Vec<QalaType> = entry.params.clone();
2104 return Some((
2105 params,
2106 entry.ret_ty.clone(),
2107 FnKey {
2108 type_name: Some(tn.clone()),
2109 name: name.to_string(),
2110 },
2111 ));
2112 }
2113 }
2114 }
2115 None
2116 })();
2117 let (params, ret_ty) = match resolved {
2118 Some((p, r, key)) => {
2119 if let Some(ctx) = &mut self.fn_ctx {
2121 ctx.called_fns.push(key.clone());
2122 if ctx.annotated_effect.is_some() {
2123 ctx.callsites_to_check.push(EffectViolationCandidate {
2124 caller_key: FnKey {
2125 type_name: ctx.type_name.clone(),
2126 name: ctx.name.clone(),
2127 },
2128 callee_key: key,
2129 call_span: span,
2130 });
2131 }
2132 }
2133 (p, r)
2134 }
2135 None => {
2136 if !matches!(receiver_ty, QalaType::Unknown) {
2137 self.errors.push(QalaError::Type {
2138 span,
2139 message: format!("no method `{name}` on type `{}`", receiver_ty.display()),
2140 });
2141 }
2142 (Vec::new(), QalaType::Unknown)
2143 }
2144 };
2145 let typed_args: Vec<typed_ast::TypedExpr> = if params.is_empty() && args.is_empty() {
2146 Vec::new()
2147 } else {
2148 args.iter()
2149 .enumerate()
2150 .map(|(i, a)| {
2151 if i < params.len() {
2152 self.check_expr(a, ¶ms[i])
2153 } else {
2154 self.infer_expr(a)
2155 }
2156 })
2157 .collect()
2158 };
2159 if args.len() != params.len()
2160 && !matches!(receiver_ty, QalaType::Unknown)
2161 && !ret_ty.types_match(&QalaType::Unknown)
2162 {
2163 self.errors.push(QalaError::Type {
2165 span,
2166 message: format!(
2167 "method `{name}` expects {} argument(s), found {}",
2168 params.len(),
2169 args.len()
2170 ),
2171 });
2172 }
2173 typed_ast::TypedExpr::MethodCall {
2174 receiver: Box::new(typed_receiver),
2175 name: name.to_string(),
2176 args: typed_args,
2177 ty: ret_ty,
2178 span,
2179 }
2180 }
2181
2182 fn check_call(
2187 &mut self,
2188 callee: &ast::Expr,
2189 args: &[ast::Expr],
2190 span: Span,
2191 ) -> typed_ast::TypedExpr {
2192 if let ast::Expr::Ident { name, .. } = callee {
2195 if let Some(ctor_ty) = self.try_builtin_constructor(name, args, span) {
2196 let typed_callee = typed_ast::TypedExpr::Ident {
2197 name: name.clone(),
2198 ty: QalaType::Function {
2199 params: Vec::new(),
2200 returns: Box::new(ctor_ty.0.clone()),
2201 },
2202 span: callee.span(),
2203 };
2204 return typed_ast::TypedExpr::Call {
2205 callee: Box::new(typed_callee),
2206 args: ctor_ty.1,
2207 ty: ctor_ty.0,
2208 span,
2209 };
2210 }
2211 if let Some((enum_name, fields)) = self.find_enum_variant(name) {
2214 let typed_args: Vec<typed_ast::TypedExpr> = args
2215 .iter()
2216 .enumerate()
2217 .map(|(i, a)| {
2218 if i < fields.len() {
2219 self.check_expr(a, &fields[i])
2220 } else {
2221 self.infer_expr(a)
2222 }
2223 })
2224 .collect();
2225 if args.len() != fields.len() {
2226 self.errors.push(QalaError::Type {
2227 span,
2228 message: format!(
2229 "variant `{name}` of `{enum_name}` expects {} argument(s), found {}",
2230 fields.len(),
2231 args.len()
2232 ),
2233 });
2234 }
2235 let typed_callee = typed_ast::TypedExpr::Ident {
2236 name: name.clone(),
2237 ty: QalaType::Function {
2238 params: fields.clone(),
2239 returns: Box::new(QalaType::Named(Symbol(enum_name.clone()))),
2240 },
2241 span: callee.span(),
2242 };
2243 return typed_ast::TypedExpr::Call {
2244 callee: Box::new(typed_callee),
2245 args: typed_args,
2246 ty: QalaType::Named(Symbol(enum_name)),
2247 span,
2248 };
2249 }
2250 if let Some((params, ret_ty, is_generic, name_clone, callee_key)) =
2252 self.resolve_free_callable(name)
2253 {
2254 if let Some(ctx) = &mut self.fn_ctx {
2256 ctx.called_fns.push(callee_key.clone());
2257 if ctx.annotated_effect.is_some() {
2258 ctx.callsites_to_check.push(EffectViolationCandidate {
2259 caller_key: FnKey {
2260 type_name: ctx.type_name.clone(),
2261 name: ctx.name.clone(),
2262 },
2263 callee_key,
2264 call_span: span,
2265 });
2266 }
2267 }
2268 let typed_callee = typed_ast::TypedExpr::Ident {
2269 name: name_clone.clone(),
2270 ty: QalaType::Function {
2271 params: params.clone(),
2272 returns: Box::new(ret_ty.clone()),
2273 },
2274 span: callee.span(),
2275 };
2276 let typed_args = self.check_call_args(args, ¶ms, is_generic, &name_clone, span);
2277 if name_clone == "abs"
2280 && let Some(arg0) = typed_args.first()
2281 {
2282 let arg_ty = arg0.ty();
2283 if !matches!(arg_ty, QalaType::I64 | QalaType::F64 | QalaType::Unknown) {
2284 self.errors.push(QalaError::TypeMismatch {
2285 span,
2286 expected: "i64 or f64".to_string(),
2287 found: arg_ty.display(),
2288 });
2289 }
2290 }
2291 let result_ty = self.resolve_generic_return_ty(&name_clone, &ret_ty, &typed_args);
2292 return typed_ast::TypedExpr::Call {
2293 callee: Box::new(typed_callee),
2294 args: typed_args,
2295 ty: result_ty,
2296 span,
2297 };
2298 }
2299 self.errors.push(QalaError::UndefinedName {
2301 span: callee.span(),
2302 name: name.clone(),
2303 });
2304 let typed_callee = typed_ast::TypedExpr::Ident {
2305 name: name.clone(),
2306 ty: QalaType::Unknown,
2307 span: callee.span(),
2308 };
2309 let typed_args: Vec<typed_ast::TypedExpr> =
2310 args.iter().map(|a| self.infer_expr(a)).collect();
2311 return typed_ast::TypedExpr::Call {
2312 callee: Box::new(typed_callee),
2313 args: typed_args,
2314 ty: QalaType::Unknown,
2315 span,
2316 };
2317 }
2318 let typed_callee = self.infer_expr(callee);
2321 let (params, ret_ty) = match typed_callee.ty() {
2322 QalaType::Function { params, returns } => (params.clone(), (**returns).clone()),
2323 _ => {
2324 self.errors.push(QalaError::Type {
2325 span,
2326 message: format!(
2327 "cannot call value of type `{}`",
2328 typed_callee.ty().display()
2329 ),
2330 });
2331 (Vec::new(), QalaType::Unknown)
2332 }
2333 };
2334 let typed_args: Vec<typed_ast::TypedExpr> = args
2335 .iter()
2336 .enumerate()
2337 .map(|(i, a)| {
2338 if i < params.len() {
2339 self.check_expr(a, ¶ms[i])
2340 } else {
2341 self.infer_expr(a)
2342 }
2343 })
2344 .collect();
2345 typed_ast::TypedExpr::Call {
2346 callee: Box::new(typed_callee),
2347 args: typed_args,
2348 ty: ret_ty,
2349 span,
2350 }
2351 }
2352
2353 fn check_call_args(
2358 &mut self,
2359 args: &[ast::Expr],
2360 params: &[QalaType],
2361 is_generic: bool,
2362 name: &str,
2363 call_span: Span,
2364 ) -> Vec<typed_ast::TypedExpr> {
2365 if args.len() != params.len() {
2366 if !is_generic {
2370 self.errors.push(QalaError::Type {
2371 span: call_span,
2372 message: format!(
2373 "function `{name}` expects {} argument(s), found {}",
2374 params.len(),
2375 args.len()
2376 ),
2377 });
2378 }
2379 }
2380 args.iter()
2381 .enumerate()
2382 .map(|(i, a)| {
2383 if !is_generic && i < params.len() {
2384 self.check_expr(a, ¶ms[i])
2385 } else {
2386 self.infer_expr(a)
2387 }
2388 })
2389 .collect()
2390 }
2391
2392 fn resolve_generic_return_ty(
2397 &self,
2398 name: &str,
2399 ret_ty: &QalaType,
2400 typed_args: &[typed_ast::TypedExpr],
2401 ) -> QalaType {
2402 match name {
2403 "len" => QalaType::I64,
2404 "push" => QalaType::Void,
2405 "abs" => match typed_args.first().map(|a| a.ty()) {
2409 Some(QalaType::I64) => QalaType::I64,
2410 Some(QalaType::F64) => QalaType::F64,
2411 _ => QalaType::Unknown,
2412 },
2413 "pop" => {
2414 if let Some(arg0) = typed_args.first()
2416 && let QalaType::Array(elem, _) = arg0.ty()
2417 {
2418 return QalaType::Option(elem.clone());
2419 }
2420 QalaType::Option(Box::new(QalaType::Unknown))
2421 }
2422 "type_of" => QalaType::Str,
2423 "map" => {
2424 let u = typed_args.get(1).and_then(|f| match f.ty() {
2426 QalaType::Function { returns, .. } => Some((**returns).clone()),
2427 _ => None,
2428 });
2429 QalaType::Array(Box::new(u.unwrap_or(QalaType::Unknown)), None)
2430 }
2431 "filter" => {
2432 if let Some(arg0) = typed_args.first() {
2434 return arg0.ty().clone();
2435 }
2436 QalaType::Array(Box::new(QalaType::Unknown), None)
2437 }
2438 "reduce" => {
2439 if let Some(arg1) = typed_args.get(1) {
2441 return arg1.ty().clone();
2442 }
2443 QalaType::Unknown
2444 }
2445 _ => ret_ty.clone(),
2446 }
2447 }
2448
2449 fn find_enum_variant(&self, variant_name: &str) -> Option<(String, Vec<QalaType>)> {
2454 for (enum_name, info) in &self.symbols.enums {
2455 for (vname, fields) in &info.variants {
2456 if vname == variant_name {
2457 return Some((enum_name.clone(), fields.clone()));
2458 }
2459 }
2460 }
2461 None
2462 }
2463
2464 #[allow(clippy::type_complexity)]
2467 fn resolve_free_callable(
2468 &self,
2469 name: &str,
2470 ) -> Option<(Vec<QalaType>, QalaType, bool, String, FnKey)> {
2471 let key = FnKey {
2472 type_name: None,
2473 name: name.to_string(),
2474 };
2475 if let Some(info) = self.symbols.fns.get(&key) {
2476 let params: Vec<QalaType> = info.params.iter().map(|(_, t, _)| t.clone()).collect();
2477 return Some((params, info.ret_ty.clone(), false, name.to_string(), key));
2478 }
2479 for entry in stdlib_signatures().iter() {
2480 if entry.type_name.is_none() && entry.name == name {
2481 return Some((
2482 entry.params.clone(),
2483 entry.ret_ty.clone(),
2484 entry.is_generic,
2485 name.to_string(),
2486 FnKey {
2487 type_name: None,
2488 name: name.to_string(),
2489 },
2490 ));
2491 }
2492 }
2493 None
2494 }
2495
2496 fn try_builtin_constructor(
2500 &mut self,
2501 name: &str,
2502 args: &[ast::Expr],
2503 span: Span,
2504 ) -> Option<(QalaType, Vec<typed_ast::TypedExpr>)> {
2505 match name {
2506 "Ok" => {
2507 if args.len() != 1 {
2508 self.errors.push(QalaError::Type {
2509 span,
2510 message: format!("`Ok` expects 1 argument, found {}", args.len()),
2511 });
2512 let typed_args: Vec<_> = args.iter().map(|a| self.infer_expr(a)).collect();
2513 return Some((
2514 QalaType::Result(Box::new(QalaType::Unknown), Box::new(QalaType::Unknown)),
2515 typed_args,
2516 ));
2517 }
2518 let typed_arg = self.infer_expr(&args[0]);
2519 let ok_ty = typed_arg.ty().clone();
2520 Some((
2521 QalaType::Result(Box::new(ok_ty), Box::new(QalaType::Unknown)),
2522 vec![typed_arg],
2523 ))
2524 }
2525 "Err" => {
2526 if args.len() != 1 {
2527 self.errors.push(QalaError::Type {
2528 span,
2529 message: format!("`Err` expects 1 argument, found {}", args.len()),
2530 });
2531 let typed_args: Vec<_> = args.iter().map(|a| self.infer_expr(a)).collect();
2532 return Some((
2533 QalaType::Result(Box::new(QalaType::Unknown), Box::new(QalaType::Unknown)),
2534 typed_args,
2535 ));
2536 }
2537 let typed_arg = self.infer_expr(&args[0]);
2538 let err_ty = typed_arg.ty().clone();
2539 Some((
2540 QalaType::Result(Box::new(QalaType::Unknown), Box::new(err_ty)),
2541 vec![typed_arg],
2542 ))
2543 }
2544 "Some" => {
2545 if args.len() != 1 {
2546 self.errors.push(QalaError::Type {
2547 span,
2548 message: format!("`Some` expects 1 argument, found {}", args.len()),
2549 });
2550 let typed_args: Vec<_> = args.iter().map(|a| self.infer_expr(a)).collect();
2551 return Some((QalaType::Option(Box::new(QalaType::Unknown)), typed_args));
2552 }
2553 let typed_arg = self.infer_expr(&args[0]);
2554 let inner = typed_arg.ty().clone();
2555 Some((QalaType::Option(Box::new(inner)), vec![typed_arg]))
2556 }
2557 "None" => {
2558 if !args.is_empty() {
2559 self.errors.push(QalaError::Type {
2560 span,
2561 message: format!("`None` expects 0 arguments, found {}", args.len()),
2562 });
2563 }
2564 let typed_args: Vec<_> = args.iter().map(|a| self.infer_expr(a)).collect();
2565 Some((QalaType::Option(Box::new(QalaType::Unknown)), typed_args))
2566 }
2567 _ => None,
2568 }
2569 }
2570
2571 fn check_binary(
2575 &mut self,
2576 op: &ast::BinOp,
2577 lhs: &ast::Expr,
2578 rhs: &ast::Expr,
2579 span: Span,
2580 ) -> typed_ast::TypedExpr {
2581 use ast::BinOp;
2582 let typed_lhs = self.infer_expr(lhs);
2583 let typed_rhs = self.infer_expr(rhs);
2584 let lty = typed_lhs.ty().clone();
2585 let rty = typed_rhs.ty().clone();
2586 let result_ty = match op {
2587 BinOp::Add => {
2588 if lty.types_match(&QalaType::Str) && rty.types_match(&QalaType::Str) {
2590 QalaType::Str
2591 } else if lty.types_match(&rty)
2592 && (lty.types_match(&QalaType::I64) || lty.types_match(&QalaType::F64))
2593 {
2594 lty.clone()
2595 } else if matches!(lty, QalaType::Unknown) || matches!(rty, QalaType::Unknown) {
2596 QalaType::Unknown
2597 } else {
2598 self.errors.push(QalaError::TypeMismatch {
2599 span,
2600 expected: lty.display(),
2601 found: rty.display(),
2602 });
2603 QalaType::Unknown
2604 }
2605 }
2606 BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Rem => {
2607 if lty.types_match(&rty)
2608 && (lty.types_match(&QalaType::I64) || lty.types_match(&QalaType::F64))
2609 {
2610 lty.clone()
2611 } else if matches!(lty, QalaType::Unknown) || matches!(rty, QalaType::Unknown) {
2612 QalaType::Unknown
2613 } else {
2614 self.errors.push(QalaError::TypeMismatch {
2615 span,
2616 expected: lty.display(),
2617 found: rty.display(),
2618 });
2619 QalaType::Unknown
2620 }
2621 }
2622 BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
2623 if !lty.types_match(&rty) {
2624 self.errors.push(QalaError::TypeMismatch {
2625 span,
2626 expected: lty.display(),
2627 found: rty.display(),
2628 });
2629 }
2630 QalaType::Bool
2631 }
2632 BinOp::Eq | BinOp::Ne => {
2633 if !lty.types_match(&rty) {
2634 self.errors.push(QalaError::TypeMismatch {
2635 span,
2636 expected: lty.display(),
2637 found: rty.display(),
2638 });
2639 }
2640 QalaType::Bool
2641 }
2642 BinOp::And | BinOp::Or => {
2643 if !lty.types_match(&QalaType::Bool) {
2644 self.errors.push(QalaError::TypeMismatch {
2645 span: lhs.span(),
2646 expected: "bool".to_string(),
2647 found: lty.display(),
2648 });
2649 }
2650 if !rty.types_match(&QalaType::Bool) {
2651 self.errors.push(QalaError::TypeMismatch {
2652 span: rhs.span(),
2653 expected: "bool".to_string(),
2654 found: rty.display(),
2655 });
2656 }
2657 QalaType::Bool
2658 }
2659 };
2660 typed_ast::TypedExpr::Binary {
2661 op: op.clone(),
2662 lhs: Box::new(typed_lhs),
2663 rhs: Box::new(typed_rhs),
2664 ty: result_ty,
2665 span,
2666 }
2667 }
2668
2669 fn check_pipeline_call(
2673 &mut self,
2674 call: &ast::Expr,
2675 lhs_ty: &QalaType,
2676 ) -> (typed_ast::TypedExpr, QalaType) {
2677 match call {
2680 ast::Expr::Ident { name, span } => {
2681 if let Some((params, ret_ty, is_generic, name_clone, key)) =
2683 self.resolve_free_callable(name)
2684 {
2685 if let Some(ctx) = &mut self.fn_ctx {
2686 ctx.called_fns.push(key.clone());
2687 if ctx.annotated_effect.is_some() {
2688 ctx.callsites_to_check.push(EffectViolationCandidate {
2689 caller_key: FnKey {
2690 type_name: ctx.type_name.clone(),
2691 name: ctx.name.clone(),
2692 },
2693 callee_key: key,
2694 call_span: *span,
2695 });
2696 }
2697 }
2698 if !is_generic && !params.is_empty() && !lhs_ty.types_match(¶ms[0]) {
2700 self.errors.push(QalaError::TypeMismatch {
2701 span: *span,
2702 expected: params[0].display(),
2703 found: lhs_ty.display(),
2704 });
2705 }
2706 let typed_call = typed_ast::TypedExpr::Ident {
2708 name: name_clone.clone(),
2709 ty: QalaType::Function {
2710 params: params.clone(),
2711 returns: Box::new(ret_ty.clone()),
2712 },
2713 span: *span,
2714 };
2715 return (typed_call, ret_ty);
2716 }
2717 self.errors.push(QalaError::UndefinedName {
2719 span: *span,
2720 name: name.clone(),
2721 });
2722 (
2723 typed_ast::TypedExpr::Ident {
2724 name: name.clone(),
2725 ty: QalaType::Unknown,
2726 span: *span,
2727 },
2728 QalaType::Unknown,
2729 )
2730 }
2731 ast::Expr::Call { callee, args, span } => {
2732 let typed_call = self.check_call(callee, args, *span);
2739 let ret_ty = typed_call.ty().clone();
2742 (typed_call, ret_ty)
2743 }
2744 other => {
2745 let typed = self.infer_expr(other);
2747 let ty = typed.ty().clone();
2748 (typed, ty)
2749 }
2750 }
2751 }
2752
2753 fn check_match(
2759 &mut self,
2760 scrutinee: &ast::Expr,
2761 arms: &[ast::MatchArm],
2762 span: Span,
2763 ) -> typed_ast::TypedExpr {
2764 let typed_scrutinee = self.infer_expr(scrutinee);
2765 let scrut_ty = typed_scrutinee.ty().clone();
2766 let mut typed_arms: Vec<typed_ast::TypedMatchArm> = Vec::with_capacity(arms.len());
2767 let mut result_ty: Option<QalaType> = None;
2768 for arm in arms {
2769 self.scopes.push(HashMap::new());
2772 self.bind_pattern(&arm.pattern, &scrut_ty);
2773 let typed_guard = arm
2774 .guard
2775 .as_ref()
2776 .map(|g| self.check_expr(g, &QalaType::Bool));
2777 let typed_body = match &arm.body {
2778 ast::MatchArmBody::Expr(e) => {
2779 let typed = self.infer_expr(e);
2780 typed_ast::TypedMatchArmBody::Expr(Box::new(typed))
2781 }
2782 ast::MatchArmBody::Block(b) => {
2783 typed_ast::TypedMatchArmBody::Block(self.check_block(b, None))
2784 }
2785 };
2786 self.scopes.pop();
2787 let body_ty = match &typed_body {
2789 typed_ast::TypedMatchArmBody::Expr(e) => e.ty().clone(),
2790 typed_ast::TypedMatchArmBody::Block(b) => b.ty.clone(),
2791 };
2792 result_ty = match (result_ty, body_ty.clone()) {
2793 (None, b) => Some(b),
2794 (Some(a), b) if a.types_match(&b) => Some(a),
2795 (Some(_), _) => Some(QalaType::Unknown),
2796 };
2797 typed_arms.push(typed_ast::TypedMatchArm {
2798 pattern: arm.pattern.clone(),
2799 guard: typed_guard,
2800 body: typed_body,
2801 span: arm.span,
2802 });
2803 }
2804 if let QalaType::Named(Symbol(enum_name)) = &scrut_ty
2806 && self.symbols.enums.contains_key(enum_name)
2807 {
2808 self.check_match_exhaustive(enum_name, arms, span);
2809 } else {
2810 let guarded_binding_arms = arms
2814 .iter()
2815 .filter(|a| matches!(a.pattern, ast::Pattern::Binding { .. }) && a.guard.is_some())
2816 .count();
2817 if guarded_binding_arms > 1 {
2818 let w = QalaWarning {
2819 category: "overlapping_guards".to_string(),
2820 message: "multiple guarded arms cover the same pattern; only the first that matches will run".to_string(),
2821 span,
2822 note: None,
2823 };
2824 self.emit_warning(w);
2825 }
2826 }
2827 let ty = result_ty.unwrap_or(QalaType::Unknown);
2828 typed_ast::TypedExpr::Match {
2829 scrutinee: Box::new(typed_scrutinee),
2830 arms: typed_arms,
2831 ty,
2832 span,
2833 }
2834 }
2835
2836 fn check_match_exhaustive(
2842 &mut self,
2843 enum_name: &str,
2844 arms: &[ast::MatchArm],
2845 match_span: Span,
2846 ) {
2847 let variants: Vec<String> = match self.symbols.enums.get(enum_name) {
2848 Some(info) => info.variants.iter().map(|(n, _)| n.clone()).collect(),
2849 None => return,
2850 };
2851 let mut covered: BTreeSet<String> = BTreeSet::new();
2852 let mut has_wildcard = false;
2853 let mut guarded_arms_by_variant: HashMap<String, u32> = HashMap::new();
2854 let mut guarded_binding_arms: u32 = 0;
2855 for arm in arms {
2856 match &arm.pattern {
2857 ast::Pattern::Variant { name, span, .. } => {
2858 if !variants.contains(name) {
2859 self.errors.push(QalaError::Type {
2860 span: *span,
2861 message: format!("variant `{name}` is not part of enum `{enum_name}`"),
2862 });
2863 continue;
2864 }
2865 if arm.guard.is_some() {
2866 *guarded_arms_by_variant.entry(name.clone()).or_insert(0) += 1;
2867 } else {
2868 covered.insert(name.clone());
2870 }
2871 }
2872 ast::Pattern::Wildcard { .. } => {
2873 has_wildcard = true;
2874 }
2875 ast::Pattern::Binding { .. } => {
2876 has_wildcard = true;
2877 if arm.guard.is_some() {
2878 guarded_binding_arms += 1;
2879 }
2880 }
2881 ast::Pattern::Int { span, .. }
2883 | ast::Pattern::Float { span, .. }
2884 | ast::Pattern::Byte { span, .. }
2885 | ast::Pattern::Str { span, .. }
2886 | ast::Pattern::Bool { span, .. } => {
2887 self.errors.push(QalaError::Type {
2888 span: *span,
2889 message: "literal pattern cannot match an enum value".to_string(),
2890 });
2891 }
2892 }
2893 }
2894 if !has_wildcard {
2895 let mut missing: Vec<String> = variants
2896 .iter()
2897 .filter(|v| !covered.contains(*v))
2898 .cloned()
2899 .collect();
2900 if !missing.is_empty() {
2901 missing.sort();
2902 self.errors.push(QalaError::NonExhaustiveMatch {
2903 span: match_span,
2904 enum_name: enum_name.to_string(),
2905 missing,
2906 });
2907 }
2908 }
2909 let any_overlap =
2912 guarded_arms_by_variant.values().any(|c| *c > 1) || guarded_binding_arms > 1;
2913 if any_overlap {
2914 let w = QalaWarning {
2915 category: "overlapping_guards".to_string(),
2916 message: "multiple guarded arms cover the same pattern; only the first that matches will run".to_string(),
2917 span: match_span,
2918 note: None,
2919 };
2920 self.emit_warning(w);
2921 }
2922 }
2923
2924 fn check_satisfies(&mut self, ty: &QalaType, interface_name: &str, use_span: Span) {
2932 let type_name = match ty {
2933 QalaType::Named(Symbol(s)) => s.clone(),
2934 _ => return,
2935 };
2936 let interface: Vec<(String, Vec<QalaType>, QalaType)> =
2937 match self.symbols.interfaces.get(interface_name) {
2938 Some(i) => i
2939 .methods
2940 .iter()
2941 .map(|m| (m.name.clone(), m.params.clone(), m.ret_ty.clone()))
2942 .collect(),
2943 None => return,
2944 };
2945 let mut missing: Vec<String> = Vec::new();
2946 let mut mismatched: Vec<(String, String, String)> = Vec::new();
2947 for (mname, mparams, mret) in interface {
2948 let key = FnKey {
2949 type_name: Some(type_name.clone()),
2950 name: mname.clone(),
2951 };
2952 let impl_info = self.symbols.fns.get(&key);
2953 match impl_info {
2954 None => missing.push(mname),
2955 Some(info) => {
2956 let impl_params: Vec<QalaType> = info
2958 .params
2959 .iter()
2960 .skip(
2961 if info
2962 .params
2963 .first()
2964 .map(|(n, _, _)| n == "self")
2965 .unwrap_or(false)
2966 {
2967 1
2968 } else {
2969 0
2970 },
2971 )
2972 .map(|(_, t, _)| t.clone())
2973 .collect();
2974 let iface_params: Vec<QalaType> = mparams
2978 .iter()
2979 .skip(if matches!(mparams.first(), Some(QalaType::Unknown)) {
2980 1
2981 } else {
2982 0
2983 })
2984 .cloned()
2985 .collect();
2986 let params_match = iface_params.len() == impl_params.len()
2987 && iface_params
2988 .iter()
2989 .zip(impl_params.iter())
2990 .all(|(a, b)| a.types_match(b));
2991 let ret_match = mret.types_match(&info.ret_ty);
2992 if !params_match || !ret_match {
2993 let expected = format_fn_sig(&iface_params, &mret);
2995 let found = format_fn_sig(&impl_params, &info.ret_ty);
2996 mismatched.push((mname, expected, found));
2997 }
2998 }
2999 }
3000 }
3001 if !missing.is_empty() || !mismatched.is_empty() {
3002 let mut missing_sorted = missing;
3004 missing_sorted.sort();
3005 mismatched.sort_by(|a, b| a.0.cmp(&b.0));
3006 self.errors.push(QalaError::InterfaceNotSatisfied {
3007 span: use_span,
3008 ty: type_name,
3009 interface: interface_name.to_string(),
3010 missing: missing_sorted,
3011 mismatched,
3012 });
3013 }
3014 }
3015
3016 fn bind_pattern(&mut self, pattern: &ast::Pattern, scrut_ty: &QalaType) {
3023 match pattern {
3024 ast::Pattern::Variant { name, sub, span } => {
3025 let mut variant_fields: Option<Vec<QalaType>> = None;
3028 if let QalaType::Named(Symbol(enum_name)) = scrut_ty
3029 && let Some(info) = self.symbols.enums.get(enum_name)
3030 {
3031 for (vname, fields) in &info.variants {
3032 if vname == name {
3033 variant_fields = Some(fields.clone());
3034 break;
3035 }
3036 }
3037 if variant_fields.is_none() {
3038 self.errors.push(QalaError::Type {
3039 span: *span,
3040 message: format!("variant `{name}` is not part of enum `{enum_name}`"),
3041 });
3042 }
3043 }
3044 if let Some(fields) = variant_fields {
3045 for (i, sub_pat) in sub.iter().enumerate() {
3046 let sub_ty = fields.get(i).cloned().unwrap_or(QalaType::Unknown);
3047 self.bind_pattern(sub_pat, &sub_ty);
3048 }
3049 } else {
3050 for sub_pat in sub {
3052 self.bind_pattern(sub_pat, &QalaType::Unknown);
3053 }
3054 }
3055 }
3056 ast::Pattern::Binding { name, span } => {
3057 if let Some(scope) = self.scopes.last_mut() {
3058 scope.insert(
3059 name.clone(),
3060 LocalInfo {
3061 ty: scrut_ty.clone(),
3062 span: *span,
3063 is_mut: false,
3064 used: false,
3067 is_param: true,
3068 },
3069 );
3070 }
3071 }
3072 _ => {}
3074 }
3075 }
3076
3077 fn resolve_function_effects(&mut self) {
3088 const MAX_ROUNDS: usize = 8;
3089 let keys: Vec<FnKey> = self.body_records.keys().cloned().collect();
3092 for key in &keys {
3093 if let Some(info) = self.symbols.fns.get_mut(key) {
3094 let intrinsic = self
3095 .body_records
3096 .get(key)
3097 .map(|r| r.intrinsic)
3098 .unwrap_or(EffectSet::pure());
3099 info.inferred_effect = Some(intrinsic);
3100 }
3101 }
3102 for round in 0..MAX_ROUNDS {
3104 let mut changed = false;
3105 for key in &keys {
3106 let body_rec = match self.body_records.get(key) {
3107 Some(r) => r,
3108 None => continue,
3109 };
3110 let mut e = body_rec.intrinsic;
3111 for callee_key in &body_rec.called {
3112 if let Some(callee_info) = self.symbols.fns.get(callee_key) {
3113 if let Some(eff) = callee_info.inferred_effect {
3114 e = e.union(eff);
3115 } else if let Some(eff) = callee_info.annotated_effect {
3116 e = e.union(eff);
3117 }
3118 } else if let Some(stdlib_eff) = stdlib_effect(callee_key) {
3119 e = e.union(stdlib_eff);
3120 }
3121 }
3122 if let Some(info_mut) = self.symbols.fns.get_mut(key) {
3123 let old = info_mut.inferred_effect.unwrap_or(EffectSet::pure());
3124 let new = old.union(e);
3125 if new != old {
3126 info_mut.inferred_effect = Some(new);
3127 changed = true;
3128 }
3129 }
3130 }
3131 if !changed {
3132 break;
3133 }
3134 debug_assert!(
3137 round < MAX_ROUNDS - 1,
3138 "effect fixed-point did not converge in {MAX_ROUNDS} rounds"
3139 );
3140 }
3141 let candidates: Vec<EffectViolationCandidate> = keys
3147 .iter()
3148 .flat_map(|k| {
3149 self.body_records
3150 .get(k)
3151 .map(|r| r.callsites_to_check.clone())
3152 .unwrap_or_default()
3153 })
3154 .collect();
3155 for cand in candidates {
3156 let caller_eff = self
3157 .symbols
3158 .fns
3159 .get(&cand.caller_key)
3160 .and_then(|info| info.annotated_effect)
3161 .unwrap_or(EffectSet::full());
3162 let callee_eff = if let Some(callee_info) = self.symbols.fns.get(&cand.callee_key) {
3163 callee_info
3164 .inferred_effect
3165 .or(callee_info.annotated_effect)
3166 .unwrap_or(EffectSet::pure())
3167 } else if let Some(eff) = stdlib_effect(&cand.callee_key) {
3168 eff
3169 } else {
3170 EffectSet::pure()
3171 };
3172 if !callee_eff.is_subset_of(caller_eff) {
3173 self.errors.push(QalaError::EffectViolation {
3174 span: cand.call_span,
3175 caller: cand.caller_key.name.clone(),
3176 caller_effect: caller_eff.display(),
3177 callee: cand.callee_key.name.clone(),
3178 callee_effect: callee_eff.display(),
3179 });
3180 }
3181 }
3182 }
3183
3184 fn emit_warning(&mut self, w: QalaWarning) {
3186 if self.is_silenced(w.span, &w.category) {
3187 return;
3188 }
3189 self.warnings.push(w);
3190 }
3191
3192 fn is_silenced(&self, span: Span, category: &str) -> bool {
3194 let line = self.line_index.location(self.src, span.start as usize).0;
3195 self.allow
3196 .get(&line)
3197 .map(|cats| cats.contains(category))
3198 .unwrap_or(false)
3199 }
3200}
3201
3202#[allow(dead_code)]
3204struct BodyEffectRecord {
3205 intrinsic: EffectSet,
3208 called: Vec<FnKey>,
3210 callsites_to_check: Vec<EffectViolationCandidate>,
3212}
3213
3214fn types_strictly_equal(a: &QalaType, b: &QalaType) -> bool {
3218 if matches!(a, QalaType::Unknown) || matches!(b, QalaType::Unknown) {
3219 return false;
3220 }
3221 a == b
3222}
3223
3224fn format_fn_sig(params: &[QalaType], ret: &QalaType) -> String {
3228 if params.is_empty() {
3229 format!("fn(self) -> {}", ret.display())
3230 } else {
3231 let p: Vec<String> = params.iter().map(|t| t.display()).collect();
3232 format!("fn(self, {}) -> {}", p.join(", "), ret.display())
3233 }
3234}
3235
3236#[allow(dead_code)]
3239struct StdlibFn {
3240 type_name: Option<String>,
3243 name: String,
3245 params: Vec<QalaType>,
3247 ret_ty: QalaType,
3249 effect: EffectSet,
3251 is_generic: bool,
3255}
3256
3257fn scan_allow_directives(src: &str) -> HashMap<usize, BTreeSet<String>> {
3271 let mut out: HashMap<usize, BTreeSet<String>> = HashMap::new();
3272 for (idx, line) in src.lines().enumerate() {
3273 let trimmed = line.trim_start();
3274 if !trimmed.starts_with("// qala: allow(") {
3275 continue;
3276 }
3277 let Some(body) = trimmed.strip_prefix("// qala: allow(") else {
3278 continue;
3279 };
3280 let Some(close_idx) = body.find(')') else {
3281 continue;
3282 };
3283 if !body[close_idx + 1..].trim().is_empty() {
3286 continue;
3287 }
3288 let cats_str = &body[..close_idx];
3289 let cats: BTreeSet<String> = cats_str
3290 .split(',')
3291 .map(|c| c.trim().to_string())
3292 .filter(|c| !c.is_empty())
3293 .collect();
3294 if cats.is_empty() {
3295 continue;
3296 }
3297 out.insert(idx + 2, cats);
3300 }
3301 out
3302}
3303
3304fn stdlib_effect(key: &FnKey) -> Option<EffectSet> {
3308 let stdlib = stdlib_signatures();
3309 for entry in stdlib.iter() {
3310 if entry.type_name == key.type_name && entry.name == key.name {
3311 return Some(entry.effect);
3312 }
3313 }
3314 None
3315}
3316
3317fn stdlib_signatures() -> Vec<StdlibFn> {
3322 vec![
3323 StdlibFn {
3324 type_name: None,
3325 name: "print".to_string(),
3326 params: vec![QalaType::Str],
3327 ret_ty: QalaType::Void,
3328 effect: EffectSet::io(),
3329 is_generic: false,
3330 },
3331 StdlibFn {
3332 type_name: None,
3333 name: "println".to_string(),
3334 params: vec![QalaType::Str],
3335 ret_ty: QalaType::Void,
3336 effect: EffectSet::io(),
3337 is_generic: false,
3338 },
3339 StdlibFn {
3340 type_name: None,
3341 name: "sqrt".to_string(),
3342 params: vec![QalaType::F64],
3343 ret_ty: QalaType::F64,
3344 effect: EffectSet::pure(),
3345 is_generic: false,
3346 },
3347 StdlibFn {
3348 type_name: None,
3349 name: "abs".to_string(),
3350 params: vec![QalaType::Unknown],
3354 ret_ty: QalaType::Unknown,
3355 effect: EffectSet::pure(),
3356 is_generic: true,
3357 },
3358 StdlibFn {
3359 type_name: None,
3360 name: "assert".to_string(),
3361 params: vec![QalaType::Bool],
3362 ret_ty: QalaType::Void,
3363 effect: EffectSet::panic(),
3364 is_generic: false,
3365 },
3366 StdlibFn {
3367 type_name: None,
3368 name: "len".to_string(),
3369 params: vec![QalaType::Array(Box::new(QalaType::Unknown), None)],
3370 ret_ty: QalaType::I64,
3371 effect: EffectSet::pure(),
3372 is_generic: true,
3373 },
3374 StdlibFn {
3375 type_name: None,
3376 name: "push".to_string(),
3377 params: vec![
3378 QalaType::Array(Box::new(QalaType::Unknown), None),
3379 QalaType::Unknown,
3380 ],
3381 ret_ty: QalaType::Void,
3382 effect: EffectSet::alloc(),
3383 is_generic: true,
3384 },
3385 StdlibFn {
3386 type_name: None,
3387 name: "pop".to_string(),
3388 params: vec![QalaType::Array(Box::new(QalaType::Unknown), None)],
3389 ret_ty: QalaType::Option(Box::new(QalaType::Unknown)),
3390 effect: EffectSet::alloc(),
3391 is_generic: true,
3392 },
3393 StdlibFn {
3394 type_name: None,
3395 name: "type_of".to_string(),
3396 params: vec![QalaType::Unknown],
3397 ret_ty: QalaType::Str,
3398 effect: EffectSet::pure(),
3399 is_generic: true,
3400 },
3401 StdlibFn {
3402 type_name: None,
3403 name: "open".to_string(),
3404 params: vec![QalaType::Str],
3405 ret_ty: QalaType::FileHandle,
3406 effect: EffectSet::io(),
3407 is_generic: false,
3408 },
3409 StdlibFn {
3410 type_name: None,
3411 name: "close".to_string(),
3412 params: vec![QalaType::FileHandle],
3413 ret_ty: QalaType::Void,
3414 effect: EffectSet::io(),
3415 is_generic: false,
3416 },
3417 StdlibFn {
3418 type_name: None,
3419 name: "map".to_string(),
3420 params: vec![
3421 QalaType::Array(Box::new(QalaType::Unknown), None),
3422 QalaType::Function {
3423 params: vec![QalaType::Unknown],
3424 returns: Box::new(QalaType::Unknown),
3425 },
3426 ],
3427 ret_ty: QalaType::Array(Box::new(QalaType::Unknown), None),
3428 effect: EffectSet::pure(),
3429 is_generic: true,
3430 },
3431 StdlibFn {
3432 type_name: None,
3433 name: "filter".to_string(),
3434 params: vec![
3435 QalaType::Array(Box::new(QalaType::Unknown), None),
3436 QalaType::Function {
3437 params: vec![QalaType::Unknown],
3438 returns: Box::new(QalaType::Bool),
3439 },
3440 ],
3441 ret_ty: QalaType::Array(Box::new(QalaType::Unknown), None),
3442 effect: EffectSet::pure(),
3443 is_generic: true,
3444 },
3445 StdlibFn {
3446 type_name: None,
3447 name: "reduce".to_string(),
3448 params: vec![
3449 QalaType::Array(Box::new(QalaType::Unknown), None),
3450 QalaType::Unknown,
3451 QalaType::Function {
3452 params: vec![QalaType::Unknown, QalaType::Unknown],
3453 returns: Box::new(QalaType::Unknown),
3454 },
3455 ],
3456 ret_ty: QalaType::Unknown,
3457 effect: EffectSet::pure(),
3458 is_generic: true,
3459 },
3460 StdlibFn {
3462 type_name: Some("FileHandle".to_string()),
3463 name: "read_all".to_string(),
3464 params: vec![],
3465 ret_ty: QalaType::Result(Box::new(QalaType::Str), Box::new(QalaType::Str)),
3466 effect: EffectSet::io(),
3467 is_generic: false,
3468 },
3469 ]
3470}
3471
3472fn resolve_type_silent(ty: &ast::TypeExpr, symbols: &SymbolTable) -> QalaType {
3478 match ty {
3479 ast::TypeExpr::Primitive { kind, .. } => QalaType::from_prim_type(kind),
3480 ast::TypeExpr::Named { name, .. } => {
3481 if name == "FileHandle" {
3482 return QalaType::FileHandle;
3483 }
3484 if symbols.structs.contains_key(name)
3485 || symbols.enums.contains_key(name)
3486 || symbols.interfaces.contains_key(name)
3487 {
3488 return QalaType::Named(Symbol(name.clone()));
3489 }
3490 QalaType::Unknown
3491 }
3492 ast::TypeExpr::Array { elem, size, .. } => QalaType::Array(
3493 Box::new(resolve_type_silent(elem, symbols)),
3494 Some(*size as usize),
3495 ),
3496 ast::TypeExpr::DynArray { elem, .. } => {
3497 QalaType::Array(Box::new(resolve_type_silent(elem, symbols)), None)
3498 }
3499 ast::TypeExpr::Tuple { elems, .. } => QalaType::Tuple(
3500 elems
3501 .iter()
3502 .map(|e| resolve_type_silent(e, symbols))
3503 .collect(),
3504 ),
3505 ast::TypeExpr::Fn { params, ret, .. } => QalaType::Function {
3506 params: params
3507 .iter()
3508 .map(|p| resolve_type_silent(p, symbols))
3509 .collect(),
3510 returns: Box::new(resolve_type_silent(ret, symbols)),
3511 },
3512 ast::TypeExpr::Generic { name, args, .. } => {
3513 if name == "Result" && args.len() == 2 {
3514 return QalaType::Result(
3515 Box::new(resolve_type_silent(&args[0], symbols)),
3516 Box::new(resolve_type_silent(&args[1], symbols)),
3517 );
3518 }
3519 if name == "Option" && args.len() == 1 {
3520 return QalaType::Option(Box::new(resolve_type_silent(&args[0], symbols)));
3521 }
3522 QalaType::Unknown
3523 }
3524 }
3525}
3526
3527#[cfg(test)]
3530mod tests {
3531 use super::*;
3532 use crate::lexer::Lexer;
3533 use crate::parser::Parser;
3534
3535 fn check(src: &str) -> (typed_ast::TypedAst, Vec<QalaError>, Vec<QalaWarning>) {
3537 let tokens = Lexer::tokenize(src).expect("lex");
3538 let ast = Parser::parse(&tokens).expect("parse");
3539 check_program(&ast, src)
3540 }
3541
3542 #[allow(dead_code)]
3545 fn check_ok(src: &str) -> typed_ast::TypedAst {
3546 let (typed, errors, _) = check(src);
3547 assert!(errors.is_empty(), "unexpected errors: {errors:?}");
3548 typed
3549 }
3550
3551 #[test]
3552 fn collect_empty_program() {
3553 let (typed, errors, warnings) = check("");
3554 assert!(typed.is_empty());
3555 assert!(errors.is_empty());
3556 assert!(warnings.is_empty());
3557 }
3558
3559 #[test]
3560 fn collect_single_fn() {
3561 let src = "fn main() is io { }";
3562 let (typed, errors, warnings) = check(src);
3563 assert!(errors.is_empty(), "{errors:?}");
3564 assert!(warnings.is_empty(), "{warnings:?}");
3565 assert_eq!(typed.len(), 1);
3566 match &typed[0] {
3567 typed_ast::TypedItem::Fn(f) => {
3568 assert_eq!(f.name, "main");
3569 assert_eq!(f.ret_ty, QalaType::Void);
3570 assert_eq!(f.effect, EffectSet::io());
3571 }
3572 _ => panic!("expected Fn, got {:?}", typed[0]),
3573 }
3574 }
3575
3576 #[test]
3577 fn collect_struct_with_two_fields() {
3578 let src = "struct A { x: i64, y: bool }";
3579 let (typed, errors, _) = check(src);
3580 assert!(errors.is_empty(), "{errors:?}");
3581 assert_eq!(typed.len(), 1);
3582 match &typed[0] {
3583 typed_ast::TypedItem::Struct(s) => {
3584 assert_eq!(s.name, "A");
3585 assert_eq!(s.fields.len(), 2);
3586 assert_eq!(s.fields[0].name, "x");
3587 assert_eq!(s.fields[0].ty, QalaType::I64);
3588 assert_eq!(s.fields[1].name, "y");
3589 assert_eq!(s.fields[1].ty, QalaType::Bool);
3590 }
3591 _ => panic!("expected Struct"),
3592 }
3593 }
3594
3595 #[test]
3596 fn collect_enum_with_three_variants() {
3597 let src = "enum Shape { Circle(f64), Rect(f64, f64), Triangle }";
3598 let (typed, errors, _) = check(src);
3599 assert!(errors.is_empty(), "{errors:?}");
3600 match &typed[0] {
3601 typed_ast::TypedItem::Enum(e) => {
3602 assert_eq!(e.name, "Shape");
3603 assert_eq!(e.variants.len(), 3);
3604 assert_eq!(e.variants[0].name, "Circle");
3605 assert_eq!(e.variants[0].fields, vec![QalaType::F64]);
3606 assert_eq!(e.variants[1].name, "Rect");
3607 assert_eq!(e.variants[1].fields, vec![QalaType::F64, QalaType::F64]);
3608 assert_eq!(e.variants[2].name, "Triangle");
3609 assert!(e.variants[2].fields.is_empty());
3610 }
3611 _ => panic!("expected Enum"),
3612 }
3613 }
3614
3615 #[test]
3616 fn collect_interface_with_one_method() {
3617 let src = "interface Printable { fn to_string(self) -> str }";
3618 let (typed, errors, _) = check(src);
3619 assert!(errors.is_empty(), "{errors:?}");
3620 match &typed[0] {
3621 typed_ast::TypedItem::Interface(i) => {
3622 assert_eq!(i.name, "Printable");
3623 assert_eq!(i.methods.len(), 1);
3624 assert_eq!(i.methods[0].name, "to_string");
3625 assert_eq!(i.methods[0].ret_ty, QalaType::Str);
3626 }
3627 _ => panic!("expected Interface"),
3628 }
3629 }
3630
3631 #[test]
3632 fn collect_struct_with_unknown_field_type() {
3633 let src = "struct A { x: Nope }";
3634 let (_, errors, _) = check(src);
3635 assert_eq!(errors.len(), 1);
3636 match &errors[0] {
3637 QalaError::UnknownType { name, .. } => {
3638 assert_eq!(name, "Nope");
3639 }
3640 other => panic!("expected UnknownType, got {other:?}"),
3641 }
3642 }
3643
3644 #[test]
3645 fn collect_recursive_struct_self_loop() {
3646 let src = "struct A { x: A }";
3647 let (_, errors, _) = check(src);
3648 let cycle_errors: Vec<&QalaError> = errors
3649 .iter()
3650 .filter(|e| matches!(e, QalaError::RecursiveStructByValue { .. }))
3651 .collect();
3652 assert_eq!(
3653 cycle_errors.len(),
3654 1,
3655 "expected exactly one cycle error: {errors:?}"
3656 );
3657 match cycle_errors[0] {
3658 QalaError::RecursiveStructByValue { path, .. } => {
3659 assert_eq!(path, &vec!["A".to_string(), "A".to_string()]);
3660 }
3661 _ => unreachable!(),
3662 }
3663 }
3664
3665 #[test]
3666 fn collect_recursive_struct_mutual() {
3667 let src = "struct A { x: B } struct B { y: A }";
3668 let (_, errors, _) = check(src);
3669 let cycle_errors: Vec<&QalaError> = errors
3670 .iter()
3671 .filter(|e| matches!(e, QalaError::RecursiveStructByValue { .. }))
3672 .collect();
3673 assert_eq!(cycle_errors.len(), 1, "expected one cycle: {errors:?}");
3674 match cycle_errors[0] {
3675 QalaError::RecursiveStructByValue { path, .. } => {
3676 assert_eq!(
3678 path,
3679 &vec!["A".to_string(), "B".to_string(), "A".to_string()]
3680 );
3681 }
3682 _ => unreachable!(),
3683 }
3684 }
3685
3686 #[test]
3687 fn collect_dynamic_array_self_reference_is_not_a_cycle() {
3688 let src = "struct A { xs: [A] }";
3690 let (_, errors, _) = check(src);
3691 let cycle_errors: Vec<&QalaError> = errors
3692 .iter()
3693 .filter(|e| matches!(e, QalaError::RecursiveStructByValue { .. }))
3694 .collect();
3695 assert!(cycle_errors.is_empty(), "no cycle expected: {errors:?}");
3696 }
3697
3698 #[test]
3699 fn collect_tuple_self_reference_is_a_cycle() {
3700 let src = "struct A { x: (A, i64) }";
3702 let (_, errors, _) = check(src);
3703 let cycle_errors: Vec<&QalaError> = errors
3704 .iter()
3705 .filter(|e| matches!(e, QalaError::RecursiveStructByValue { .. }))
3706 .collect();
3707 assert_eq!(cycle_errors.len(), 1, "expected cycle: {errors:?}");
3708 }
3709
3710 #[test]
3711 fn collect_fn_with_unknown_param_type() {
3712 let src = "fn f(x: Nope) -> i64 is pure { return 0 }";
3713 let (_, errors, _) = check(src);
3714 let unknown_errors: Vec<&QalaError> = errors
3715 .iter()
3716 .filter(|e| matches!(e, QalaError::UnknownType { .. }))
3717 .collect();
3718 assert!(!unknown_errors.is_empty());
3719 }
3720
3721 #[test]
3722 fn collect_duplicate_struct_definition() {
3723 let src = "struct A { x: i64 } struct A { y: bool }";
3724 let (_, errors, _) = check(src);
3725 let dup: Vec<&QalaError> = errors
3727 .iter()
3728 .filter(
3729 |e| matches!(e, QalaError::Type { message, .. } if message.contains("duplicate")),
3730 )
3731 .collect();
3732 assert_eq!(dup.len(), 1, "{errors:?}");
3733 }
3734
3735 #[test]
3736 fn collect_errors_are_sorted_by_span() {
3737 let src = "struct A { x: Nope } struct A { y: i64 }";
3741 let (_, errors, _) = check(src);
3742 let starts: Vec<u32> = errors.iter().map(|e| e.span().start).collect();
3744 let mut sorted = starts.clone();
3745 sorted.sort();
3746 assert_eq!(
3747 starts, sorted,
3748 "errors not sorted by span.start: {errors:?}"
3749 );
3750 }
3751
3752 #[test]
3755 fn infer_local_let_types() {
3756 let src = "fn main() is io { let x = 42; println(\"hi\") }";
3758 let (_, errors, _) = check(src);
3759 let type_errors: Vec<&QalaError> = errors
3761 .iter()
3762 .filter(|e| !matches!(e, QalaError::UndefinedName { .. }))
3763 .collect();
3764 assert!(type_errors.is_empty(), "{errors:?}");
3765 }
3766
3767 #[test]
3768 fn let_with_wrong_annotation_emits_type_mismatch() {
3769 let src = "fn main() is io { let x: i64 = \"hello\"; println(\"\") }";
3771 let (_, errors, _) = check(src);
3772 let mismatch: Vec<&QalaError> = errors
3773 .iter()
3774 .filter(|e| matches!(e, QalaError::TypeMismatch { .. }))
3775 .collect();
3776 assert!(!mismatch.is_empty(), "expected TypeMismatch in {errors:?}");
3777 }
3778
3779 #[test]
3780 fn redundant_annotation_warns() {
3781 let src = "fn main() is io { let x: i64 = 42; println(\"{x}\") }";
3783 let (_, errors, warnings) = check(src);
3784 assert!(errors.is_empty(), "{errors:?}");
3785 let red: Vec<&QalaWarning> = warnings
3786 .iter()
3787 .filter(|w| w.category == "redundant_annotation")
3788 .collect();
3789 assert_eq!(red.len(), 1, "{warnings:?}");
3790 assert!(
3791 red[0].message.contains("redundant type annotation"),
3792 "{red:?}"
3793 );
3794 }
3795
3796 #[test]
3797 fn undefined_name_in_initializer_emits_one_error() {
3798 let src = "fn main() is io { let x = nope; println(\"\") }";
3799 let (_, errors, _) = check(src);
3800 let undef: Vec<&QalaError> = errors
3801 .iter()
3802 .filter(|e| matches!(e, QalaError::UndefinedName { name, .. } if name == "nope"))
3803 .collect();
3804 assert_eq!(undef.len(), 1, "{errors:?}");
3805 }
3806
3807 #[test]
3808 fn arg_type_mismatch_message() {
3809 let src = "fn f(x: i64) -> str is pure { x }";
3811 let (_, errors, _) = check(src);
3812 let mismatch: Vec<&QalaError> = errors
3813 .iter()
3814 .filter(|e| matches!(e, QalaError::TypeMismatch { .. }))
3815 .collect();
3816 assert!(!mismatch.is_empty(), "expected TypeMismatch in {errors:?}");
3817 match mismatch[0] {
3818 QalaError::TypeMismatch {
3819 expected, found, ..
3820 } => {
3821 assert_eq!(expected, "str");
3822 assert_eq!(found, "i64");
3823 }
3824 _ => unreachable!(),
3825 }
3826 }
3827
3828 #[test]
3829 fn missing_return_at_last_expr() {
3830 let src = "fn f() -> i64 is pure { }";
3832 let (_, errors, _) = check(src);
3833 let missing: Vec<&QalaError> = errors
3834 .iter()
3835 .filter(|e| matches!(e, QalaError::MissingReturn { .. }))
3836 .collect();
3837 assert_eq!(missing.len(), 1, "{errors:?}");
3838 }
3839
3840 #[test]
3841 fn fn_with_correct_trailing_value_passes() {
3842 let src = "fn f(x: i64) -> i64 is pure { x }";
3843 let (_, errors, _) = check(src);
3844 assert!(errors.is_empty(), "{errors:?}");
3845 }
3846
3847 #[test]
3848 fn fn_with_explicit_return_passes() {
3849 let src = "fn f(x: i64) -> i64 is pure { return x }";
3850 let (_, errors, _) = check(src);
3851 assert!(errors.is_empty(), "{errors:?}");
3852 }
3853
3854 #[test]
3855 fn fn_return_with_wrong_type() {
3856 let src = "fn f(x: i64) -> i64 is pure { return \"oops\" }";
3857 let (_, errors, _) = check(src);
3858 let mismatch: Vec<&QalaError> = errors
3859 .iter()
3860 .filter(|e| matches!(e, QalaError::TypeMismatch { .. }))
3861 .collect();
3862 assert!(!mismatch.is_empty(), "{errors:?}");
3863 }
3864
3865 #[test]
3866 fn or_fallback_typechecks_success() {
3867 let src = r#"
3869 fn lookup() -> Option<i64> is pure { return Some(1) }
3870 fn main() is io {
3871 let r = lookup() or 0
3872 println("{r}")
3873 }
3874 "#;
3875 let (_, errors, _) = check(src);
3876 let mismatch: Vec<&QalaError> = errors
3877 .iter()
3878 .filter(|e| matches!(e, QalaError::TypeMismatch { .. }))
3879 .collect();
3880 assert!(mismatch.is_empty(), "{errors:?}");
3881 }
3882
3883 #[test]
3884 fn or_fallback_wrong_type_errors_at_fallback() {
3885 let src = r#"
3886 fn lookup() -> Option<i64> is pure { return Some(1) }
3887 fn main() is io {
3888 let r = lookup() or "oops"
3889 println("{r}")
3890 }
3891 "#;
3892 let (_, errors, _) = check(src);
3893 let mismatch: Vec<&QalaError> = errors
3894 .iter()
3895 .filter(|e| matches!(e, QalaError::TypeMismatch { .. }))
3896 .collect();
3897 assert!(!mismatch.is_empty(), "{errors:?}");
3898 }
3899
3900 #[test]
3901 fn question_legal_inside_result_fn() {
3902 let src = r#"
3903 fn parse_int(s: str) -> Result<i64, str> is pure { return Ok(0) }
3904 fn f(s: str) -> Result<i64, str> is pure {
3905 let x = parse_int(s)?
3906 return Ok(x)
3907 }
3908 "#;
3909 let (_, errors, _) = check(src);
3910 let redundant: Vec<&QalaError> = errors
3911 .iter()
3912 .filter(|e| matches!(e, QalaError::RedundantQuestionOperator { .. }))
3913 .collect();
3914 assert!(redundant.is_empty(), "{errors:?}");
3915 }
3916
3917 #[test]
3918 fn question_in_non_result_fn_errors() {
3919 let src = r#"
3920 fn parse_int(s: str) -> Result<i64, str> is pure { return Ok(0) }
3921 fn f(s: str) -> i64 is pure {
3922 let x = parse_int(s)?
3923 return x
3924 }
3925 "#;
3926 let (_, errors, _) = check(src);
3927 let redundant: Vec<&QalaError> = errors
3928 .iter()
3929 .filter(|e| matches!(e, QalaError::RedundantQuestionOperator { .. }))
3930 .collect();
3931 assert_eq!(redundant.len(), 1, "{errors:?}");
3932 }
3933
3934 #[test]
3935 fn struct_literal_unknown_field_errors() {
3936 let src = r#"
3937 struct Point { x: i64, y: i64 }
3938 fn main() is io {
3939 let p = Point { x: 1, z: 2 }
3940 println("ok")
3941 }
3942 "#;
3943 let (_, errors, _) = check(src);
3944 let type_errors: Vec<&QalaError> = errors
3946 .iter()
3947 .filter(|e| matches!(e, QalaError::Type { .. }))
3948 .collect();
3949 assert!(type_errors.len() >= 2, "{errors:?}");
3950 }
3951
3952 #[test]
3953 fn method_call_resolves_user_method() {
3954 let src = r#"
3955 struct Point { x: f64, y: f64 }
3956 fn Point.distance(self) -> f64 is pure { return 0.0 }
3957 fn main() is io {
3958 let p = Point { x: 1.0, y: 2.0 }
3959 let d = p.distance()
3960 println("{d}")
3961 }
3962 "#;
3963 let (_, errors, _) = check(src);
3964 assert!(errors.is_empty(), "{errors:?}");
3965 }
3966
3967 #[test]
3968 fn index_into_fixed_array_types_to_elem() {
3969 let src = r#"
3970 fn main() is io {
3971 let arr = [1, 2, 3]
3972 let v = arr[0]
3973 println("{v}")
3974 }
3975 "#;
3976 let (_, errors, _) = check(src);
3977 assert!(errors.is_empty(), "{errors:?}");
3978 }
3979
3980 #[test]
3981 fn index_with_string_errors() {
3982 let src = r#"
3983 fn main() is io {
3984 let arr = [1, 2, 3]
3985 let v = arr["x"]
3986 println("{v}")
3987 }
3988 "#;
3989 let (_, errors, _) = check(src);
3990 let mismatch: Vec<&QalaError> = errors
3991 .iter()
3992 .filter(|e| matches!(e, QalaError::TypeMismatch { .. }))
3993 .collect();
3994 assert!(!mismatch.is_empty(), "{errors:?}");
3995 }
3996
3997 #[test]
3998 fn pipeline_with_unary_callee_types_through() {
3999 let src = r#"
4000 fn double(x: i64) -> i64 is pure { return x * 2 }
4001 fn main() is io {
4002 let r = 5 |> double
4003 println("{r}")
4004 }
4005 "#;
4006 let (_, errors, _) = check(src);
4007 assert!(errors.is_empty(), "{errors:?}");
4008 }
4009
4010 #[test]
4011 fn interpolation_resolves_inner_expression() {
4012 let src = r#"
4013 fn main() is io {
4014 let name = "world"
4015 println("hello, {name}!")
4016 }
4017 "#;
4018 let (_, errors, _) = check(src);
4019 assert!(errors.is_empty(), "{errors:?}");
4020 }
4021
4022 #[test]
4023 fn binary_arithmetic_on_matching_types_passes() {
4024 let src = r#"
4025 fn add(a: i64, b: i64) -> i64 is pure { return a + b }
4026 "#;
4027 let (_, errors, _) = check(src);
4028 assert!(errors.is_empty(), "{errors:?}");
4029 }
4030
4031 #[test]
4032 fn binary_arithmetic_on_mismatched_types_errors() {
4033 let src = r#"
4034 fn bad(a: i64, b: str) -> i64 is pure { return a + b }
4035 "#;
4036 let (_, errors, _) = check(src);
4037 let mismatch: Vec<&QalaError> = errors
4038 .iter()
4039 .filter(|e| matches!(e, QalaError::TypeMismatch { .. }))
4040 .collect();
4041 assert!(!mismatch.is_empty(), "{errors:?}");
4042 }
4043
4044 #[test]
4045 fn comparison_returns_bool() {
4046 let src = r#"
4047 fn lt(a: i64, b: i64) -> bool is pure { return a < b }
4048 "#;
4049 let (_, errors, _) = check(src);
4050 assert!(errors.is_empty(), "{errors:?}");
4051 }
4052
4053 #[test]
4054 fn boolean_ops_require_bool_operands() {
4055 let src = r#"
4056 fn bad() -> bool is pure { return 1 && 2 }
4057 "#;
4058 let (_, errors, _) = check(src);
4059 let mismatch: Vec<&QalaError> = errors
4060 .iter()
4061 .filter(|e| matches!(e, QalaError::TypeMismatch { .. }))
4062 .collect();
4063 assert!(mismatch.len() >= 2, "{errors:?}");
4065 }
4066
4067 #[test]
4068 fn unreachable_after_return_warns_once() {
4069 let src = r#"
4073 fn main() is io {
4074 return
4075 let x = 1
4076 }
4077 "#;
4078 let (_, _, warnings) = check(src);
4079 let unreach: Vec<&QalaWarning> = warnings
4080 .iter()
4081 .filter(|w| w.category == "unreachable_code")
4082 .collect();
4083 assert_eq!(unreach.len(), 1, "{warnings:?}");
4084 }
4085
4086 #[test]
4089 fn pure_add_function_has_no_effect_errors() {
4090 let src = "fn add(a: i64, b: i64) -> i64 is pure { a + b }";
4091 let (_, errors, _) = check(src);
4092 assert!(errors.is_empty(), "{errors:?}");
4093 }
4094
4095 #[test]
4096 fn pure_calls_io_errors_with_hint() {
4097 let src = r#"
4098 fn shout(msg: str) is pure {
4099 println(msg)
4100 }
4101 "#;
4102 let (_, errors, _) = check(src);
4103 let viol: Vec<&QalaError> = errors
4104 .iter()
4105 .filter(|e| matches!(e, QalaError::EffectViolation { .. }))
4106 .collect();
4107 assert_eq!(viol.len(), 1, "{errors:?}");
4108 match viol[0] {
4109 QalaError::EffectViolation {
4110 caller,
4111 caller_effect,
4112 callee,
4113 callee_effect,
4114 ..
4115 } => {
4116 assert_eq!(caller, "shout");
4117 assert_eq!(caller_effect, "pure");
4118 assert_eq!(callee, "println");
4119 assert_eq!(callee_effect, "io");
4120 }
4121 _ => unreachable!(),
4122 }
4123 }
4124
4125 #[test]
4126 fn unannotated_caller_inherits_io_effect() {
4127 let src = r#"
4128 fn echo(msg: str) {
4129 println(msg)
4130 }
4131 "#;
4132 let (typed, errors, _) = check(src);
4133 assert!(errors.is_empty(), "{errors:?}");
4134 assert!(matches!(&typed[0], typed_ast::TypedItem::Fn(_)));
4139 }
4140
4141 #[test]
4142 fn mutual_recursion_effects_converge() {
4143 let src = r#"
4147 fn a(n: i64) -> i64 {
4148 if n == 0 { return 0 }
4149 return b(n - 1)
4150 }
4151 fn b(n: i64) -> i64 {
4152 if n == 0 { return 0 }
4153 println("b")
4154 return a(n - 1)
4155 }
4156 "#;
4157 let (_, errors, _) = check(src);
4158 let viol: Vec<&QalaError> = errors
4159 .iter()
4160 .filter(|e| matches!(e, QalaError::EffectViolation { .. }))
4161 .collect();
4162 assert!(
4163 viol.is_empty(),
4164 "no annotated callers; no violations: {errors:?}"
4165 );
4166 }
4167
4168 #[test]
4169 fn match_missing_variants_listed() {
4170 let src = r#"
4171 enum Shape { Circle(f64), Rect(f64, f64), Triangle }
4172 fn area(s: Shape) -> f64 is pure {
4173 match s {
4174 Circle(r) => r,
4175 Rect(w, h) => w * h,
4176 }
4177 }
4178 "#;
4179 let (_, errors, _) = check(src);
4180 let nonex: Vec<&QalaError> = errors
4181 .iter()
4182 .filter(|e| matches!(e, QalaError::NonExhaustiveMatch { .. }))
4183 .collect();
4184 assert_eq!(nonex.len(), 1, "{errors:?}");
4185 match nonex[0] {
4186 QalaError::NonExhaustiveMatch {
4187 enum_name, missing, ..
4188 } => {
4189 assert_eq!(enum_name, "Shape");
4190 assert_eq!(missing, &vec!["Triangle".to_string()]);
4191 }
4192 _ => unreachable!(),
4193 }
4194 }
4195
4196 #[test]
4197 fn match_with_wildcard_is_exhaustive() {
4198 let src = r#"
4199 enum Shape { Circle(f64), Rect(f64, f64), Triangle }
4200 fn area(s: Shape) -> f64 is pure {
4201 match s {
4202 Circle(r) => r,
4203 _ => 0.0,
4204 }
4205 }
4206 "#;
4207 let (_, errors, _) = check(src);
4208 let nonex: Vec<&QalaError> = errors
4209 .iter()
4210 .filter(|e| matches!(e, QalaError::NonExhaustiveMatch { .. }))
4211 .collect();
4212 assert!(nonex.is_empty(), "{errors:?}");
4213 }
4214
4215 #[test]
4216 fn match_with_all_variants_is_exhaustive() {
4217 let src = r#"
4218 enum Shape { Circle(f64), Rect(f64, f64), Triangle }
4219 fn area(s: Shape) -> f64 is pure {
4220 match s {
4221 Circle(r) => r,
4222 Rect(w, h) => w * h,
4223 Triangle => 0.0,
4224 }
4225 }
4226 "#;
4227 let (_, errors, _) = check(src);
4228 let nonex: Vec<&QalaError> = errors
4229 .iter()
4230 .filter(|e| matches!(e, QalaError::NonExhaustiveMatch { .. }))
4231 .collect();
4232 assert!(nonex.is_empty(), "{errors:?}");
4233 }
4234
4235 #[test]
4236 fn overlapping_guards_warn() {
4237 let src = r#"
4239 fn classify(v: i64) -> str is pure {
4240 match v {
4241 x if x > 0 => "pos",
4242 x if x > 10 => "big",
4243 _ => "other",
4244 }
4245 }
4246 "#;
4247 let (_, _, warnings) = check(src);
4248 let over: Vec<&QalaWarning> = warnings
4249 .iter()
4250 .filter(|w| w.category == "overlapping_guards")
4251 .collect();
4252 assert_eq!(over.len(), 1, "{warnings:?}");
4253 }
4254
4255 #[test]
4256 fn variant_not_in_enum_errors() {
4257 let src = r#"
4258 enum Shape { Circle(f64) }
4259 fn f(s: Shape) -> f64 is pure {
4260 match s {
4261 Square(x) => x,
4262 _ => 0.0,
4263 }
4264 }
4265 "#;
4266 let (_, errors, _) = check(src);
4267 let type_errors: Vec<&QalaError> = errors
4268 .iter()
4269 .filter(|e| matches!(e, QalaError::Type { message, .. } if message.contains("Square")))
4270 .collect();
4271 assert!(!type_errors.is_empty(), "{errors:?}");
4272 }
4273
4274 #[test]
4275 fn interface_satisfied_structurally() {
4276 let src = r#"
4277 interface Printable { fn to_string(self) -> str }
4278 struct Point { x: f64, y: f64 }
4279 fn Point.to_string(self) -> str is pure { return "p" }
4280 fn main() is io {
4281 let p: Printable = Point { x: 1.0, y: 2.0 }
4282 println("ok")
4283 }
4284 "#;
4285 let (_, errors, _) = check(src);
4286 let ifaces: Vec<&QalaError> = errors
4287 .iter()
4288 .filter(|e| matches!(e, QalaError::InterfaceNotSatisfied { .. }))
4289 .collect();
4290 assert!(ifaces.is_empty(), "{errors:?}");
4291 }
4292
4293 #[test]
4294 fn interface_mismatch_lists_methods() {
4295 let src = r#"
4297 interface Printable { fn to_string(self) -> str }
4298 struct Point { x: f64, y: f64 }
4299 fn main() is io {
4300 let p: Printable = Point { x: 1.0, y: 2.0 }
4301 println("ok")
4302 }
4303 "#;
4304 let (_, errors, _) = check(src);
4305 let ifaces: Vec<&QalaError> = errors
4306 .iter()
4307 .filter(|e| matches!(e, QalaError::InterfaceNotSatisfied { .. }))
4308 .collect();
4309 assert_eq!(ifaces.len(), 1, "{errors:?}");
4310 match ifaces[0] {
4311 QalaError::InterfaceNotSatisfied {
4312 ty,
4313 interface,
4314 missing,
4315 ..
4316 } => {
4317 assert_eq!(ty, "Point");
4318 assert_eq!(interface, "Printable");
4319 assert!(missing.contains(&"to_string".to_string()));
4320 }
4321 _ => unreachable!(),
4322 }
4323 }
4324
4325 #[test]
4328 fn unused_var_warns_with_correct_category() {
4329 let src = "fn main() is io { let x = 1; println(\"hi\") }";
4330 let (_, _, warnings) = check(src);
4331 let w: Vec<&QalaWarning> = warnings
4332 .iter()
4333 .filter(|w| w.category == "unused_var")
4334 .collect();
4335 assert_eq!(w.len(), 1, "{warnings:?}");
4336 assert!(w[0].message.contains("`x`"), "{w:?}");
4337 }
4338
4339 #[test]
4340 fn underscore_prefixed_name_exempt_from_unused_var() {
4341 let src = "fn main() is io { let _ = 1; println(\"hi\") }";
4342 let (_, _, warnings) = check(src);
4343 let w: Vec<&QalaWarning> = warnings
4344 .iter()
4345 .filter(|w| w.category == "unused_var")
4346 .collect();
4347 assert!(w.is_empty(), "{warnings:?}");
4348 }
4349
4350 #[test]
4351 fn function_params_exempt_from_unused_var() {
4352 let src = "fn f(x: i64) -> i64 is pure { 1 }";
4354 let (_, _, warnings) = check(src);
4355 let w: Vec<&QalaWarning> = warnings
4356 .iter()
4357 .filter(|w| w.category == "unused_var")
4358 .collect();
4359 assert!(w.is_empty(), "{warnings:?}");
4360 }
4361
4362 #[test]
4363 fn shadowed_var_warns_with_prior_binding_note() {
4364 let src = r#"
4365 fn main() is io {
4366 let x = 1
4367 {
4368 let x = 2
4369 println("{x}")
4370 }
4371 }
4372 "#;
4373 let (_, _, warnings) = check(src);
4374 let w: Vec<&QalaWarning> = warnings
4375 .iter()
4376 .filter(|w| w.category == "shadowed_var")
4377 .collect();
4378 assert_eq!(w.len(), 1, "{warnings:?}");
4379 assert!(w[0].note.is_some(), "{w:?}");
4380 assert!(
4381 w[0].note.as_ref().unwrap().contains("prior binding"),
4382 "{w:?}"
4383 );
4384 }
4385
4386 #[test]
4387 fn redundant_annotation_warns_for_matching_inferred_type() {
4388 let src = r#"
4389 fn main() -> i64 is pure {
4390 let x: i64 = 42
4391 x
4392 }
4393 "#;
4394 let (_, errors, warnings) = check(src);
4395 assert!(errors.is_empty(), "{errors:?}");
4396 let w: Vec<&QalaWarning> = warnings
4397 .iter()
4398 .filter(|w| w.category == "redundant_annotation")
4399 .collect();
4400 assert_eq!(w.len(), 1, "{warnings:?}");
4401 }
4402
4403 #[test]
4404 fn unmatched_defer_warns() {
4405 let src = r#"
4406 fn main() is io {
4407 let f = open("x.txt")
4408 println("hi")
4409 }
4410 "#;
4411 let (_, _, warnings) = check(src);
4412 let w: Vec<&QalaWarning> = warnings
4413 .iter()
4414 .filter(|w| w.category == "unmatched_defer")
4415 .collect();
4416 assert_eq!(w.len(), 1, "{warnings:?}");
4417 }
4418
4419 #[test]
4420 fn unmatched_defer_silenced_by_call_form_close() {
4421 let src = r#"
4422 fn main() is io {
4423 let f = open("x.txt")
4424 defer close(f)
4425 println("hi")
4426 }
4427 "#;
4428 let (_, _, warnings) = check(src);
4429 let w: Vec<&QalaWarning> = warnings
4430 .iter()
4431 .filter(|w| w.category == "unmatched_defer")
4432 .collect();
4433 assert!(w.is_empty(), "{warnings:?}");
4434 }
4435
4436 #[test]
4437 fn unmatched_defer_silenced_by_method_form_close() {
4438 let src = r#"
4439 fn main() is io {
4440 let f = open("x.txt")
4441 defer f.close()
4442 println("hi")
4443 }
4444 "#;
4445 let (_, _, warnings) = check(src);
4446 let w: Vec<&QalaWarning> = warnings
4447 .iter()
4448 .filter(|w| w.category == "unmatched_defer")
4449 .collect();
4450 assert!(w.is_empty(), "{warnings:?}");
4451 }
4452
4453 #[test]
4454 fn unmatched_defer_fires_per_handle_independently() {
4455 let src = r#"
4456 fn main() is io {
4457 let f = open("x.txt")
4458 let g = open("y.txt")
4459 defer close(f)
4460 println("hi")
4461 }
4462 "#;
4463 let (_, _, warnings) = check(src);
4464 let w: Vec<&QalaWarning> = warnings
4465 .iter()
4466 .filter(|w| w.category == "unmatched_defer")
4467 .collect();
4468 assert_eq!(w.len(), 1, "{warnings:?}");
4469 assert!(w[0].message.contains("`g`"), "{w:?}");
4471 }
4472
4473 #[test]
4474 fn unreachable_code_fires_after_return_keyword_followed_by_stmt_head() {
4475 let src = r#"
4476 fn main() is io {
4477 return
4478 let x = 1
4479 }
4480 "#;
4481 let (_, _, warnings) = check(src);
4482 let w: Vec<&QalaWarning> = warnings
4483 .iter()
4484 .filter(|w| w.category == "unreachable_code")
4485 .collect();
4486 assert_eq!(w.len(), 1, "{warnings:?}");
4487 assert!(w[0].message.contains("unreachable statement"), "{w:?}");
4488 }
4489
4490 #[test]
4491 fn unreachable_code_fires_exactly_once_per_block() {
4492 let src = r#"
4494 fn main() is io {
4495 return
4496 let x = 1
4497 let y = 2
4498 }
4499 "#;
4500 let (_, _, warnings) = check(src);
4501 let w: Vec<&QalaWarning> = warnings
4502 .iter()
4503 .filter(|w| w.category == "unreachable_code")
4504 .collect();
4505 assert_eq!(w.len(), 1, "{warnings:?}");
4506 }
4507
4508 #[test]
4511 fn directive_scanner_handles_section_8_edge_cases() {
4512 let t = scan_allow_directives("// qala: allow(unused_var)\nlet x = 1");
4514 assert!(t.get(&2).map(|s| s.contains("unused_var")).unwrap_or(false));
4515 let t = scan_allow_directives("let x = 1 // qala: allow(unused_var)");
4517 assert!(t.is_empty());
4518 let t = scan_allow_directives("// qala: allow(unused_var, shadowed_var)\nlet x = 1");
4520 let line2 = t.get(&2).expect("row 3 must populate line 2");
4521 assert!(line2.contains("unused_var"));
4522 assert!(line2.contains("shadowed_var"));
4523 let t = scan_allow_directives("// qala: allow( unused_var ,shadowed_var )\nlet x = 1");
4525 let line2 = t.get(&2).expect("row 4 must populate line 2");
4526 assert!(line2.contains("unused_var"));
4527 assert!(line2.contains("shadowed_var"));
4528 let t = scan_allow_directives("// qala: allow()\nlet x = 1");
4530 assert!(t.is_empty());
4531 let t = scan_allow_directives("// qala: allow(unused_var) trailing\nlet x = 1");
4533 assert!(t.is_empty());
4534 let t = scan_allow_directives("let msg = \"// qala: allow(unused_var)\"");
4536 assert!(t.is_empty());
4537 let t = scan_allow_directives("let msg = \"...\"\n// qala: allow(unused_var)\nlet x = 1");
4539 assert!(t.get(&3).map(|s| s.contains("unused_var")).unwrap_or(false));
4540 }
4541
4542 #[test]
4543 fn directive_silences_unused_var() {
4544 let src = "// qala: allow(unused_var)\nfn main() is io { let x = 1; println(\"hi\") }";
4545 let (_, _, warnings) = check(src);
4546 let w: Vec<&QalaWarning> = warnings
4547 .iter()
4548 .filter(|w| w.category == "unused_var")
4549 .collect();
4550 assert!(!w.is_empty() || warnings.is_empty(), "{warnings:?}");
4554 }
4555
4556 #[test]
4557 fn directive_silences_unused_var_on_following_line() {
4558 let src = "fn main() is io {\n// qala: allow(unused_var)\nlet x = 1; println(\"hi\") }";
4560 let (_, _, warnings) = check(src);
4561 let w: Vec<&QalaWarning> = warnings
4562 .iter()
4563 .filter(|w| w.category == "unused_var")
4564 .collect();
4565 assert!(w.is_empty(), "{warnings:?}");
4566 }
4567
4568 #[test]
4569 fn directive_silences_shadowed_var() {
4570 let src = "fn main() is io {\nlet x = 1\n// qala: allow(shadowed_var)\n{ let x = 2; println(\"{x}\") }\n}";
4571 let (_, _, warnings) = check(src);
4572 let w: Vec<&QalaWarning> = warnings
4573 .iter()
4574 .filter(|w| w.category == "shadowed_var")
4575 .collect();
4576 assert!(w.is_empty(), "{warnings:?}");
4577 }
4578
4579 #[test]
4580 fn directive_silences_redundant_annotation() {
4581 let src = "fn main() -> i64 is pure {\n// qala: allow(redundant_annotation)\nlet x: i64 = 42\nx\n}";
4582 let (_, _, warnings) = check(src);
4583 let w: Vec<&QalaWarning> = warnings
4584 .iter()
4585 .filter(|w| w.category == "redundant_annotation")
4586 .collect();
4587 assert!(w.is_empty(), "{warnings:?}");
4588 }
4589
4590 #[test]
4591 fn directive_silences_unmatched_defer() {
4592 let src = "fn main() is io {\n// qala: allow(unmatched_defer)\nlet f = open(\"x.txt\")\nprintln(\"hi\")\n}";
4593 let (_, _, warnings) = check(src);
4594 let w: Vec<&QalaWarning> = warnings
4595 .iter()
4596 .filter(|w| w.category == "unmatched_defer")
4597 .collect();
4598 assert!(w.is_empty(), "{warnings:?}");
4599 }
4600
4601 #[test]
4602 fn directive_silences_unreachable_code() {
4603 let src = "fn main() is io {\nreturn\n// qala: allow(unreachable_code)\nlet x = 1\n}";
4604 let (_, _, warnings) = check(src);
4605 let w: Vec<&QalaWarning> = warnings
4606 .iter()
4607 .filter(|w| w.category == "unreachable_code")
4608 .collect();
4609 assert!(w.is_empty(), "{warnings:?}");
4610 }
4611
4612 #[test]
4613 fn errors_are_never_silenced_by_directive() {
4614 let src = "// qala: allow(unused_var)\nfn main() is io { let x: i64 = \"oops\"; println(\"{x}\") }";
4616 let (_, errors, _) = check(src);
4617 let m: Vec<&QalaError> = errors
4618 .iter()
4619 .filter(|e| matches!(e, QalaError::TypeMismatch { .. }))
4620 .collect();
4621 assert!(!m.is_empty(), "errors must not be silenced: {errors:?}");
4622 }
4623
4624 #[test]
4625 fn multiple_directive_lines_silence_independent_lines() {
4626 let src = "fn main() is io {\n// qala: allow(unused_var)\nlet x = 1\nlet y = 2\nprintln(\"hi\")\n}";
4628 let (_, _, warnings) = check(src);
4629 let unused: Vec<&QalaWarning> = warnings
4631 .iter()
4632 .filter(|w| w.category == "unused_var")
4633 .collect();
4634 assert_eq!(unused.len(), 1, "{warnings:?}");
4635 assert!(unused[0].message.contains("`y`"), "{unused:?}");
4636 }
4637
4638 #[test]
4639 fn unmatched_defer_fires_in_innermost_else_if_branch() {
4640 let src = r#"
4643fn main(a: bool, b: bool, c: bool) is io {
4644 if a {
4645 let x = 1
4646 } else if b {
4647 let y = 2
4648 } else if c {
4649 let f = open("x.txt")
4650 }
4651}
4652"#;
4653 let (_, _, warnings) = check(src);
4654 let unmatched: Vec<&QalaWarning> = warnings
4655 .iter()
4656 .filter(|w| w.category == "unmatched_defer")
4657 .collect();
4658 assert_eq!(
4659 unmatched.len(),
4660 1,
4661 "expected one unmatched_defer in innermost else-if: {warnings:?}"
4662 );
4663 assert!(
4664 unmatched[0].message.contains("`f`"),
4665 "warning should name the handle: {:?}",
4666 unmatched[0].message
4667 );
4668 }
4669
4670 #[test]
4671 fn enum_variant_lookup_is_deterministic_across_runs() {
4672 let src = r#"
4677enum Beta { Mark, Other }
4678enum Alpha { Mark, Stuff }
4679fn f() -> Alpha is pure { return Mark }
4680"#;
4681 let (_, errors, _) = check(src);
4682 let relevant: Vec<&QalaError> = errors
4686 .iter()
4687 .filter(|e| {
4688 matches!(
4689 e,
4690 QalaError::UndefinedName { .. } | QalaError::TypeMismatch { .. }
4691 )
4692 })
4693 .collect();
4694 assert!(
4695 relevant.is_empty(),
4696 "unexpected errors with deterministic enum lookup: {relevant:?}"
4697 );
4698 }
4699
4700 #[test]
4701 fn abs_resolves_to_i64_for_int_arg_and_f64_for_float_arg() {
4702 let src_int = "fn main() -> i64 is pure { return abs(-3) }";
4704 let (typed, errors, _) = check(src_int);
4705 assert!(errors.is_empty(), "abs(i64) errors: {errors:?}");
4706 match &typed[0] {
4707 typed_ast::TypedItem::Fn(f) => {
4708 assert_eq!(f.ret_ty, QalaType::I64);
4709 }
4710 _ => panic!("expected Fn"),
4711 }
4712 let src_float = "fn main() -> f64 is pure { return abs(1.5) }";
4714 let (typed2, errors2, _) = check(src_float);
4715 assert!(errors2.is_empty(), "abs(f64) errors: {errors2:?}");
4716 match &typed2[0] {
4717 typed_ast::TypedItem::Fn(f) => {
4718 assert_eq!(f.ret_ty, QalaType::F64);
4719 }
4720 _ => panic!("expected Fn"),
4721 }
4722 }
4723
4724 #[test]
4725 fn abs_rejects_non_numeric_arg() {
4726 let src = "fn main() -> bool is pure { return abs(true) }";
4729 let (_, errors, _) = check(src);
4730 let mismatch: Vec<&QalaError> = errors
4731 .iter()
4732 .filter(|e| matches!(e, QalaError::TypeMismatch { .. }))
4733 .collect();
4734 assert!(
4735 !mismatch.is_empty(),
4736 "abs(bool) should produce TypeMismatch: {errors:?}"
4737 );
4738 }
4739
4740 #[test]
4741 fn zero_param_method_sig_has_no_trailing_comma() {
4742 let src = r#"
4746interface Reader { fn read_all(self) -> Result<str, str> }
4747struct MyReader { }
4748fn MyReader.read_all(self) -> i64 is pure { return 0 }
4749fn use_it(r: MyReader) is pure { }
4750"#;
4751 let (_, errors, _) = check(src);
4752 let iface_errs: Vec<&QalaError> = errors
4754 .iter()
4755 .filter(|e| matches!(e, QalaError::InterfaceNotSatisfied { .. }))
4756 .collect();
4757 if !iface_errs.is_empty() {
4759 match iface_errs[0] {
4760 QalaError::InterfaceNotSatisfied { mismatched, .. } => {
4761 for (_, expected_sig, found_sig) in mismatched {
4762 assert!(
4763 !expected_sig.contains(", )"),
4764 "expected sig has trailing comma: {expected_sig:?}"
4765 );
4766 assert!(
4767 !found_sig.contains(", )"),
4768 "found sig has trailing comma: {found_sig:?}"
4769 );
4770 }
4771 }
4772 _ => unreachable!(),
4773 }
4774 }
4775 let sig = format_fn_sig(&[], &QalaType::I64);
4777 assert_eq!(sig, "fn(self) -> i64", "zero-param sig: {sig:?}");
4778 let sig2 = format_fn_sig(&[QalaType::Str], &QalaType::Bool);
4780 assert_eq!(sig2, "fn(self, str) -> bool", "one-param sig: {sig2:?}");
4781 }
4782
4783 #[test]
4784 fn non_exhaustive_match_missing_list_is_alphabetically_sorted() {
4785 let src = r#"
4789enum Fruit { Zebra(i64), Apple(i64), Mango(i64) }
4790fn f(v: Fruit) -> i64 is pure {
4791 match v {
4792 Zebra(n) => n
4793 }
4794}
4795"#;
4796 let (_, errors, _) = check(src);
4797 let non_ex: Vec<&QalaError> = errors
4798 .iter()
4799 .filter(|e| matches!(e, QalaError::NonExhaustiveMatch { .. }))
4800 .collect();
4801 assert_eq!(
4802 non_ex.len(),
4803 1,
4804 "expected exactly one NonExhaustiveMatch: {errors:?}"
4805 );
4806 match non_ex[0] {
4807 QalaError::NonExhaustiveMatch { missing, .. } => {
4808 assert_eq!(missing, &vec!["Apple".to_string(), "Mango".to_string()]);
4809 }
4810 _ => unreachable!(),
4811 }
4812 }
4813
4814 #[test]
4815 fn six_bundled_examples_typecheck_without_errors() {
4816 for name in [
4819 "hello",
4820 "fibonacci",
4821 "effects",
4822 "pattern-matching",
4823 "pipeline",
4824 "defer-demo",
4825 ] {
4826 let path = format!(
4827 "{}/../../playground/public/examples/{}.qala",
4828 env!("CARGO_MANIFEST_DIR"),
4829 name
4830 );
4831 let src = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
4832 let tokens = crate::lexer::Lexer::tokenize(&src).expect("lex");
4833 let ast = crate::parser::Parser::parse(&tokens).expect("parse");
4834 let (_, errors, _warnings) = check_program(&ast, &src);
4835 assert!(
4836 errors.is_empty(),
4837 "{name}.qala: unexpected errors: {errors:?}"
4838 );
4839 }
4840 }
4841}