1use crate::ast::Span;
8use crate::ast::ddl::VectorMetric;
9use crate::ast::expr::{BinaryOp, Expr, ExprKind, Literal, UnaryOp};
10use crate::catalog::{Catalog, TableMetadata};
11use crate::planner::error::PlannerError;
12use crate::planner::typed_expr::{TypedExpr, TypedExprKind};
13use crate::planner::types::ResolvedType;
14
15pub struct TypeChecker<'a, C: Catalog + ?Sized> {
30 catalog: &'a C,
31}
32
33impl<'a, C: Catalog + ?Sized> TypeChecker<'a, C> {
34 pub fn new(catalog: &'a C) -> Self {
36 Self { catalog }
37 }
38
39 pub fn catalog(&self) -> &'a C {
41 self.catalog
42 }
43
44 pub fn infer_type(
56 &self,
57 expr: &Expr,
58 table: &TableMetadata,
59 ) -> Result<TypedExpr, PlannerError> {
60 let span = expr.span;
61 match &expr.kind {
62 ExprKind::Literal(lit) => self.infer_literal_type(lit, span),
63
64 ExprKind::ColumnRef {
65 table: table_qualifier,
66 column,
67 } => {
68 if let Some(qualifier) = table_qualifier
70 && qualifier != &table.name
71 {
72 return Err(PlannerError::TableNotFound {
73 name: qualifier.clone(),
74 line: span.start.line,
75 column: span.start.column,
76 });
77 }
78 self.infer_column_ref_type(table, column, span)
79 }
80
81 ExprKind::BinaryOp { left, op, right } => {
82 self.infer_binary_op_type(left, *op, right, table, span)
83 }
84
85 ExprKind::UnaryOp { op, operand } => {
86 self.infer_unary_op_type(*op, operand, table, span)
87 }
88
89 ExprKind::FunctionCall { name, args } => {
90 self.infer_function_call_type(name, args, table, span)
91 }
92
93 ExprKind::Between {
94 expr,
95 low,
96 high,
97 negated,
98 } => self.infer_between_type(expr, low, high, *negated, table, span),
99
100 ExprKind::Like {
101 expr,
102 pattern,
103 escape,
104 negated,
105 } => self.infer_like_type(expr, pattern, escape.as_deref(), *negated, table, span),
106
107 ExprKind::InList {
108 expr,
109 list,
110 negated,
111 } => self.infer_in_list_type(expr, list, *negated, table, span),
112
113 ExprKind::IsNull { expr, negated } => {
114 self.infer_is_null_type(expr, *negated, table, span)
115 }
116
117 ExprKind::VectorLiteral(values) => self.infer_vector_literal_type(values, span),
118 }
119 }
120
121 fn infer_literal_type(&self, lit: &Literal, span: Span) -> Result<TypedExpr, PlannerError> {
123 let (kind, resolved_type) = match lit {
124 Literal::Number(s) => {
125 let resolved_type = if s.contains('.') || s.contains('e') || s.contains('E') {
127 ResolvedType::Double
128 } else {
129 if s.parse::<i32>().is_ok() {
131 ResolvedType::Integer
132 } else {
133 ResolvedType::BigInt
134 }
135 };
136 (TypedExprKind::Literal(lit.clone()), resolved_type)
137 }
138 Literal::String(_) => (TypedExprKind::Literal(lit.clone()), ResolvedType::Text),
139 Literal::Boolean(_) => (TypedExprKind::Literal(lit.clone()), ResolvedType::Boolean),
140 Literal::Null => (TypedExprKind::Literal(lit.clone()), ResolvedType::Null),
141 };
142
143 Ok(TypedExpr {
144 kind,
145 resolved_type,
146 span,
147 })
148 }
149
150 fn infer_column_ref_type(
152 &self,
153 table: &TableMetadata,
154 column_name: &str,
155 span: Span,
156 ) -> Result<TypedExpr, PlannerError> {
157 let (column_index, column) = table
159 .columns
160 .iter()
161 .enumerate()
162 .find(|(_, c)| c.name == column_name)
163 .ok_or_else(|| PlannerError::ColumnNotFound {
164 column: column_name.to_string(),
165 table: table.name.clone(),
166 line: span.start.line,
167 col: span.start.column,
168 })?;
169
170 Ok(TypedExpr {
171 kind: TypedExprKind::ColumnRef {
172 table: table.name.clone(),
173 column: column_name.to_string(),
174 column_index,
175 },
176 resolved_type: column.data_type.clone(),
177 span,
178 })
179 }
180
181 fn infer_binary_op_type(
183 &self,
184 left: &Expr,
185 op: BinaryOp,
186 right: &Expr,
187 table: &TableMetadata,
188 span: Span,
189 ) -> Result<TypedExpr, PlannerError> {
190 let left_typed = self.infer_type(left, table)?;
191 let right_typed = self.infer_type(right, table)?;
192
193 let result_type = self.check_binary_op(
194 op,
195 &left_typed.resolved_type,
196 &right_typed.resolved_type,
197 span,
198 )?;
199
200 Ok(TypedExpr {
201 kind: TypedExprKind::BinaryOp {
202 left: Box::new(left_typed),
203 op,
204 right: Box::new(right_typed),
205 },
206 resolved_type: result_type,
207 span,
208 })
209 }
210
211 pub fn check_binary_op(
223 &self,
224 op: BinaryOp,
225 left: &ResolvedType,
226 right: &ResolvedType,
227 span: Span,
228 ) -> Result<ResolvedType, PlannerError> {
229 use BinaryOp::*;
230 use ResolvedType::*;
231
232 match op {
233 Add | Sub | Mul | Div | Mod => {
235 let result = self.check_arithmetic_op(left, right, span)?;
236 Ok(result)
237 }
238
239 Eq | Neq | Lt | Gt | LtEq | GtEq => {
241 self.check_comparison_op(left, right, span)?;
242 Ok(Boolean)
243 }
244
245 And | Or => {
247 self.check_logical_op(left, right, span)?;
248 Ok(Boolean)
249 }
250
251 StringConcat => {
253 self.check_string_concat_op(left, right, span)?;
254 Ok(Text)
255 }
256 }
257 }
258
259 fn check_arithmetic_op(
261 &self,
262 left: &ResolvedType,
263 right: &ResolvedType,
264 span: Span,
265 ) -> Result<ResolvedType, PlannerError> {
266 use ResolvedType::*;
267
268 if matches!(left, Null) || matches!(right, Null) {
270 return Ok(Null);
271 }
272
273 match (left, right) {
275 (Integer, Integer) => Ok(Integer),
277 (Integer, BigInt) | (BigInt, Integer) | (BigInt, BigInt) => Ok(BigInt),
278 (Integer, Float) | (Float, Integer) | (Float, Float) => Ok(Float),
279 (Integer, Double)
280 | (Double, Integer)
281 | (BigInt, Float)
282 | (Float, BigInt)
283 | (BigInt, Double)
284 | (Double, BigInt)
285 | (Float, Double)
286 | (Double, Float)
287 | (Double, Double) => Ok(Double),
288
289 _ => Err(PlannerError::InvalidOperator {
290 op: "arithmetic".to_string(),
291 type_name: format!("{} and {}", left.type_name(), right.type_name()),
292 line: span.start.line,
293 column: span.start.column,
294 }),
295 }
296 }
297
298 fn check_comparison_op(
300 &self,
301 left: &ResolvedType,
302 right: &ResolvedType,
303 span: Span,
304 ) -> Result<(), PlannerError> {
305 use ResolvedType::*;
306
307 if matches!(left, Null) || matches!(right, Null) {
309 return Ok(());
310 }
311
312 let compatible = match (left, right) {
314 (a, b) if a == b => true,
316
317 (Integer | BigInt | Float | Double, Integer | BigInt | Float | Double) => true,
319
320 (Text, Text) => true,
322
323 (Boolean, Boolean) => true,
325
326 (Timestamp, Timestamp) => true,
328
329 (Vector { dimension: d1, .. }, Vector { dimension: d2, .. }) => d1 == d2,
331
332 _ => false,
333 };
334
335 if compatible {
336 Ok(())
337 } else {
338 Err(PlannerError::TypeMismatch {
339 expected: left.type_name().to_string(),
340 found: right.type_name().to_string(),
341 line: span.start.line,
342 column: span.start.column,
343 })
344 }
345 }
346
347 fn check_logical_op(
349 &self,
350 left: &ResolvedType,
351 right: &ResolvedType,
352 span: Span,
353 ) -> Result<(), PlannerError> {
354 use ResolvedType::*;
355
356 let left_ok = matches!(left, Boolean | Null);
358 let right_ok = matches!(right, Boolean | Null);
359
360 if !left_ok {
361 return Err(PlannerError::TypeMismatch {
362 expected: "Boolean".to_string(),
363 found: left.type_name().to_string(),
364 line: span.start.line,
365 column: span.start.column,
366 });
367 }
368
369 if !right_ok {
370 return Err(PlannerError::TypeMismatch {
371 expected: "Boolean".to_string(),
372 found: right.type_name().to_string(),
373 line: span.start.line,
374 column: span.start.column,
375 });
376 }
377
378 Ok(())
379 }
380
381 fn check_string_concat_op(
383 &self,
384 left: &ResolvedType,
385 right: &ResolvedType,
386 span: Span,
387 ) -> Result<(), PlannerError> {
388 use ResolvedType::*;
389
390 let left_ok = matches!(left, Text | Null);
392 let right_ok = matches!(right, Text | Null);
393
394 if !left_ok {
395 return Err(PlannerError::TypeMismatch {
396 expected: "Text".to_string(),
397 found: left.type_name().to_string(),
398 line: span.start.line,
399 column: span.start.column,
400 });
401 }
402
403 if !right_ok {
404 return Err(PlannerError::TypeMismatch {
405 expected: "Text".to_string(),
406 found: right.type_name().to_string(),
407 line: span.start.line,
408 column: span.start.column,
409 });
410 }
411
412 Ok(())
413 }
414
415 fn infer_unary_op_type(
417 &self,
418 op: UnaryOp,
419 operand: &Expr,
420 table: &TableMetadata,
421 span: Span,
422 ) -> Result<TypedExpr, PlannerError> {
423 let operand_typed = self.infer_type(operand, table)?;
424
425 let result_type = match op {
426 UnaryOp::Not => {
427 if !matches!(
429 operand_typed.resolved_type,
430 ResolvedType::Boolean | ResolvedType::Null
431 ) {
432 return Err(PlannerError::TypeMismatch {
433 expected: "Boolean".to_string(),
434 found: operand_typed.resolved_type.type_name().to_string(),
435 line: span.start.line,
436 column: span.start.column,
437 });
438 }
439 ResolvedType::Boolean
440 }
441 UnaryOp::Minus => {
442 match &operand_typed.resolved_type {
444 ResolvedType::Integer => ResolvedType::Integer,
445 ResolvedType::BigInt => ResolvedType::BigInt,
446 ResolvedType::Float => ResolvedType::Float,
447 ResolvedType::Double => ResolvedType::Double,
448 ResolvedType::Null => ResolvedType::Null,
449 other => {
450 return Err(PlannerError::InvalidOperator {
451 op: "unary minus".to_string(),
452 type_name: other.type_name().to_string(),
453 line: span.start.line,
454 column: span.start.column,
455 });
456 }
457 }
458 }
459 };
460
461 Ok(TypedExpr {
462 kind: TypedExprKind::UnaryOp {
463 op,
464 operand: Box::new(operand_typed),
465 },
466 resolved_type: result_type,
467 span,
468 })
469 }
470
471 fn infer_function_call_type(
473 &self,
474 name: &str,
475 args: &[Expr],
476 table: &TableMetadata,
477 span: Span,
478 ) -> Result<TypedExpr, PlannerError> {
479 let typed_args: Vec<TypedExpr> = args
481 .iter()
482 .map(|arg| self.infer_type(arg, table))
483 .collect::<Result<Vec<_>, _>>()?;
484
485 let result_type = self.check_function_call(name, &typed_args, span)?;
487
488 Ok(TypedExpr {
489 kind: TypedExprKind::FunctionCall {
490 name: name.to_string(),
491 args: typed_args,
492 },
493 resolved_type: result_type,
494 span,
495 })
496 }
497
498 fn infer_between_type(
500 &self,
501 expr: &Expr,
502 low: &Expr,
503 high: &Expr,
504 negated: bool,
505 table: &TableMetadata,
506 span: Span,
507 ) -> Result<TypedExpr, PlannerError> {
508 let expr_typed = self.infer_type(expr, table)?;
509 let low_typed = self.infer_type(low, table)?;
510 let high_typed = self.infer_type(high, table)?;
511
512 self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
514 self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
515
516 Ok(TypedExpr {
517 kind: TypedExprKind::Between {
518 expr: Box::new(expr_typed),
519 low: Box::new(low_typed),
520 high: Box::new(high_typed),
521 negated,
522 },
523 resolved_type: ResolvedType::Boolean,
524 span,
525 })
526 }
527
528 fn infer_like_type(
530 &self,
531 expr: &Expr,
532 pattern: &Expr,
533 escape: Option<&Expr>,
534 negated: bool,
535 table: &TableMetadata,
536 span: Span,
537 ) -> Result<TypedExpr, PlannerError> {
538 let expr_typed = self.infer_type(expr, table)?;
539 let pattern_typed = self.infer_type(pattern, table)?;
540
541 if !matches!(
543 expr_typed.resolved_type,
544 ResolvedType::Text | ResolvedType::Null
545 ) {
546 return Err(PlannerError::TypeMismatch {
547 expected: "Text".to_string(),
548 found: expr_typed.resolved_type.type_name().to_string(),
549 line: expr.span.start.line,
550 column: expr.span.start.column,
551 });
552 }
553
554 if !matches!(
556 pattern_typed.resolved_type,
557 ResolvedType::Text | ResolvedType::Null
558 ) {
559 return Err(PlannerError::TypeMismatch {
560 expected: "Text".to_string(),
561 found: pattern_typed.resolved_type.type_name().to_string(),
562 line: pattern.span.start.line,
563 column: pattern.span.start.column,
564 });
565 }
566
567 let escape_typed = if let Some(esc) = escape {
568 let typed = self.infer_type(esc, table)?;
569 if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
570 return Err(PlannerError::TypeMismatch {
571 expected: "Text".to_string(),
572 found: typed.resolved_type.type_name().to_string(),
573 line: esc.span.start.line,
574 column: esc.span.start.column,
575 });
576 }
577 Some(Box::new(typed))
578 } else {
579 None
580 };
581
582 Ok(TypedExpr {
583 kind: TypedExprKind::Like {
584 expr: Box::new(expr_typed),
585 pattern: Box::new(pattern_typed),
586 escape: escape_typed,
587 negated,
588 },
589 resolved_type: ResolvedType::Boolean,
590 span,
591 })
592 }
593
594 fn infer_in_list_type(
596 &self,
597 expr: &Expr,
598 list: &[Expr],
599 negated: bool,
600 table: &TableMetadata,
601 span: Span,
602 ) -> Result<TypedExpr, PlannerError> {
603 let expr_typed = self.infer_type(expr, table)?;
604
605 let typed_list: Vec<TypedExpr> = list
606 .iter()
607 .map(|item| {
608 let typed = self.infer_type(item, table)?;
609 self.check_comparison_op(
611 &expr_typed.resolved_type,
612 &typed.resolved_type,
613 item.span,
614 )?;
615 Ok(typed)
616 })
617 .collect::<Result<Vec<_>, PlannerError>>()?;
618
619 Ok(TypedExpr {
620 kind: TypedExprKind::InList {
621 expr: Box::new(expr_typed),
622 list: typed_list,
623 negated,
624 },
625 resolved_type: ResolvedType::Boolean,
626 span,
627 })
628 }
629
630 fn infer_is_null_type(
632 &self,
633 expr: &Expr,
634 negated: bool,
635 table: &TableMetadata,
636 span: Span,
637 ) -> Result<TypedExpr, PlannerError> {
638 let expr_typed = self.infer_type(expr, table)?;
639
640 Ok(TypedExpr {
641 kind: TypedExprKind::IsNull {
642 expr: Box::new(expr_typed),
643 negated,
644 },
645 resolved_type: ResolvedType::Boolean,
646 span,
647 })
648 }
649
650 fn infer_vector_literal_type(
652 &self,
653 values: &[f64],
654 span: Span,
655 ) -> Result<TypedExpr, PlannerError> {
656 Ok(TypedExpr {
657 kind: TypedExprKind::VectorLiteral(values.to_vec()),
658 resolved_type: ResolvedType::Vector {
659 dimension: values.len() as u32,
660 metric: VectorMetric::Cosine, },
662 span,
663 })
664 }
665
666 pub fn normalize_metric(&self, metric: &str, span: Span) -> Result<VectorMetric, PlannerError> {
678 match metric.to_lowercase().as_str() {
679 "cosine" => Ok(VectorMetric::Cosine),
680 "l2" => Ok(VectorMetric::L2),
681 "inner" => Ok(VectorMetric::Inner),
682 _ => Err(PlannerError::InvalidMetric {
683 value: metric.to_string(),
684 line: span.start.line,
685 column: span.start.column,
686 }),
687 }
688 }
689
690 pub fn check_function_call(
695 &self,
696 name: &str,
697 args: &[TypedExpr],
698 span: Span,
699 ) -> Result<ResolvedType, PlannerError> {
700 let lower_name = name.to_lowercase();
701
702 match lower_name.as_str() {
703 "vector_distance" => self.check_vector_distance(args, span),
704 "vector_similarity" => self.check_vector_similarity(args, span),
705 _ => {
707 Err(PlannerError::UnsupportedFeature {
709 feature: format!("function '{}'", name),
710 version: "future".to_string(),
711 line: span.start.line,
712 column: span.start.column,
713 })
714 }
715 }
716 }
717
718 pub fn check_vector_distance(
729 &self,
730 args: &[TypedExpr],
731 span: Span,
732 ) -> Result<ResolvedType, PlannerError> {
733 if args.len() != 3 {
734 return Err(PlannerError::TypeMismatch {
735 expected: "3 arguments".to_string(),
736 found: format!("{} arguments", args.len()),
737 line: span.start.line,
738 column: span.start.column,
739 });
740 }
741
742 let col_dim = match &args[0].resolved_type {
744 ResolvedType::Vector { dimension, .. } => *dimension,
745 other => {
746 return Err(PlannerError::TypeMismatch {
747 expected: "Vector".to_string(),
748 found: other.type_name().to_string(),
749 line: args[0].span.start.line,
750 column: args[0].span.start.column,
751 });
752 }
753 };
754
755 let vec_dim = match &args[1].resolved_type {
757 ResolvedType::Vector { dimension, .. } => *dimension,
758 other => {
759 return Err(PlannerError::TypeMismatch {
760 expected: "Vector".to_string(),
761 found: other.type_name().to_string(),
762 line: args[1].span.start.line,
763 column: args[1].span.start.column,
764 });
765 }
766 };
767
768 self.check_vector_dimension(col_dim, vec_dim, args[1].span)?;
770
771 match &args[2].resolved_type {
773 ResolvedType::Text => {
774 if let TypedExprKind::Literal(Literal::String(s)) = &args[2].kind {
776 self.normalize_metric(s, args[2].span)?;
777 }
778 }
779 ResolvedType::Null => {
780 return Err(PlannerError::TypeMismatch {
782 expected: "Text (metric)".to_string(),
783 found: "Null".to_string(),
784 line: args[2].span.start.line,
785 column: args[2].span.start.column,
786 });
787 }
788 other => {
789 return Err(PlannerError::TypeMismatch {
790 expected: "Text (metric)".to_string(),
791 found: other.type_name().to_string(),
792 line: args[2].span.start.line,
793 column: args[2].span.start.column,
794 });
795 }
796 }
797
798 Ok(ResolvedType::Double)
799 }
800
801 pub fn check_vector_similarity(
807 &self,
808 args: &[TypedExpr],
809 span: Span,
810 ) -> Result<ResolvedType, PlannerError> {
811 self.check_vector_distance(args, span)
813 }
814
815 pub fn check_vector_dimension(
821 &self,
822 expected: u32,
823 found: u32,
824 span: Span,
825 ) -> Result<(), PlannerError> {
826 if expected != found {
827 Err(PlannerError::VectorDimensionMismatch {
828 expected,
829 found,
830 line: span.start.line,
831 column: span.start.column,
832 })
833 } else {
834 Ok(())
835 }
836 }
837
838 pub fn check_insert_values(
861 &self,
862 table: &TableMetadata,
863 columns: &[String],
864 values: &[Vec<Expr>],
865 span: Span,
866 ) -> Result<Vec<Vec<TypedExpr>>, PlannerError> {
867 let target_columns: Vec<&str> = if columns.is_empty() {
869 table.column_names()
870 } else {
871 columns.iter().map(|s| s.as_str()).collect()
872 };
873
874 let mut typed_rows = Vec::with_capacity(values.len());
875
876 for row in values {
877 if row.len() != target_columns.len() {
879 return Err(PlannerError::ColumnValueCountMismatch {
880 columns: target_columns.len(),
881 values: row.len(),
882 line: span.start.line,
883 column: span.start.column,
884 });
885 }
886
887 let mut typed_values = Vec::with_capacity(row.len());
888
889 for (value, col_name) in row.iter().zip(target_columns.iter()) {
890 let col_meta =
892 table
893 .get_column(col_name)
894 .ok_or_else(|| PlannerError::ColumnNotFound {
895 column: col_name.to_string(),
896 table: table.name.clone(),
897 line: span.start.line,
898 col: span.start.column,
899 })?;
900
901 let typed_value = self.infer_type(value, table)?;
903
904 self.check_null_constraint(col_meta, &typed_value, value.span)?;
906
907 self.check_type_compatibility(
909 &col_meta.data_type,
910 &typed_value.resolved_type,
911 value.span,
912 )?;
913
914 if let (
916 ResolvedType::Vector {
917 dimension: expected_dim,
918 ..
919 },
920 ResolvedType::Vector {
921 dimension: actual_dim,
922 ..
923 },
924 ) = (&col_meta.data_type, &typed_value.resolved_type)
925 {
926 self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
927 }
928
929 typed_values.push(typed_value);
930 }
931
932 typed_rows.push(typed_values);
933 }
934
935 Ok(typed_rows)
936 }
937
938 pub fn check_assignment(
949 &self,
950 table: &TableMetadata,
951 column: &str,
952 value: &Expr,
953 span: Span,
954 ) -> Result<TypedExpr, PlannerError> {
955 let col_meta = table
957 .get_column(column)
958 .ok_or_else(|| PlannerError::ColumnNotFound {
959 column: column.to_string(),
960 table: table.name.clone(),
961 line: span.start.line,
962 col: span.start.column,
963 })?;
964
965 let typed_value = self.infer_type(value, table)?;
967
968 self.check_null_constraint(col_meta, &typed_value, value.span)?;
970
971 self.check_type_compatibility(&col_meta.data_type, &typed_value.resolved_type, value.span)?;
973
974 if let (
976 ResolvedType::Vector {
977 dimension: expected_dim,
978 ..
979 },
980 ResolvedType::Vector {
981 dimension: actual_dim,
982 ..
983 },
984 ) = (&col_meta.data_type, &typed_value.resolved_type)
985 {
986 self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
987 }
988
989 Ok(typed_value)
990 }
991
992 pub fn check_null_constraint(
999 &self,
1000 column: &crate::catalog::ColumnMetadata,
1001 value: &TypedExpr,
1002 span: Span,
1003 ) -> Result<(), PlannerError> {
1004 if column.not_null && matches!(value.resolved_type, ResolvedType::Null) {
1005 Err(PlannerError::NullConstraintViolation {
1006 column: column.name.clone(),
1007 line: span.start.line,
1008 col: span.start.column,
1009 })
1010 } else {
1011 Ok(())
1012 }
1013 }
1014
1015 fn check_type_compatibility(
1023 &self,
1024 expected: &ResolvedType,
1025 actual: &ResolvedType,
1026 span: Span,
1027 ) -> Result<(), PlannerError> {
1028 if expected == actual {
1030 return Ok(());
1031 }
1032
1033 if actual.can_cast_to(expected) {
1035 return Ok(());
1036 }
1037
1038 if let (
1041 ResolvedType::Vector {
1042 dimension: d1,
1043 metric: _,
1044 },
1045 ResolvedType::Vector {
1046 dimension: d2,
1047 metric: _,
1048 },
1049 ) = (expected, actual)
1050 {
1051 if *d1 == *d2 {
1053 return Ok(());
1054 }
1055 }
1057
1058 Err(PlannerError::TypeMismatch {
1059 expected: expected.type_name().to_string(),
1060 found: actual.type_name().to_string(),
1061 line: span.start.line,
1062 column: span.start.column,
1063 })
1064 }
1065}
1066
1067#[cfg(test)]
1069#[path = "type_checker/tests.rs"]
1070mod tests;