From 62d83e303147e18cc0165b2430e7745cc46cce79 Mon Sep 17 00:00:00 2001 From: Chibuikem Michael Ilonze Date: Thu, 22 Jan 2026 13:50:26 +0100 Subject: [PATCH 1/2] feat: Implement TypeInfoKind::Error for type poisoning - Add TypeInfoKind::Error variant to represent unresolved or invalid types - Update TypeChecker to gracefully handle TypeInfoKind::Error by suppressing cascading errors - Update expression inference (Identifier, Struct, MemberAccess, FunctionCall, etc.) to return Error type on failure instead of None/cascading - Prevent spurious type mismatch errors when one of the types is Error --- core/type-checker/src/type_checker.rs | 220 ++++++++++++++++++-------- core/type-checker/src/type_info.rs | 13 +- 2 files changed, 166 insertions(+), 67 deletions(-) diff --git a/core/type-checker/src/type_checker.rs b/core/type-checker/src/type_checker.rs index 82de1b2..d75f987 100644 --- a/core/type-checker/src/type_checker.rs +++ b/core/type-checker/src/type_checker.rs @@ -606,15 +606,15 @@ impl TypeChecker { } } else { let value_type = self.infer_expression(&right_expr, ctx); - if let (Some(target), Some(val)) = (target_type, value_type) - && target != val - { - self.errors.push(TypeCheckError::TypeMismatch { - expected: target, - found: val, - context: TypeMismatchContext::Assignment, - location: assign_statement.location, - }); + if let (Some(target), Some(val)) = (target_type, value_type) { + if !target.is_error() && !val.is_error() && target != val { + self.errors.push(TypeCheckError::TypeMismatch { + expected: target, + found: val, + context: TypeMismatchContext::Assignment, + location: assign_statement.location, + }); + } } } } @@ -640,10 +640,11 @@ impl TypeChecker { } else { let value_type = self.infer_expression(&return_statement.expression.borrow(), ctx); - if *return_type != value_type.clone().unwrap_or_default() { + let val = value_type.unwrap_or_default(); + if !return_type.is_error() && !val.is_error() && *return_type != val { self.errors.push(TypeCheckError::TypeMismatch { expected: return_type.clone(), - found: value_type.unwrap_or_default(), + found: val, context: TypeMismatchContext::Return, location: return_statement.location, }); @@ -653,12 +654,19 @@ impl TypeChecker { Statement::Loop(loop_statement) => { if let Some(condition) = &*loop_statement.condition.borrow() { let condition_type = self.infer_expression(condition, ctx); - if condition_type.is_none() - || condition_type.as_ref().unwrap().kind != TypeInfoKind::Bool - { + if let Some(cond_type) = condition_type { + if !cond_type.is_error() && !cond_type.is_bool() { + self.errors.push(TypeCheckError::TypeMismatch { + expected: TypeInfo::boolean(), + found: cond_type, + context: TypeMismatchContext::Condition, + location: loop_statement.location, + }); + } + } else { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), - found: condition_type.unwrap_or_default(), + found: TypeInfo::default(), context: TypeMismatchContext::Condition, location: loop_statement.location, }); @@ -673,12 +681,19 @@ impl TypeChecker { Statement::Break(_) => {} Statement::If(if_statement) => { let condition_type = self.infer_expression(&if_statement.condition.borrow(), ctx); - if condition_type.is_none() - || condition_type.as_ref().unwrap().kind != TypeInfoKind::Bool - { + if let Some(cond_type) = condition_type { + if !cond_type.is_error() && !cond_type.is_bool() { + self.errors.push(TypeCheckError::TypeMismatch { + expected: TypeInfo::boolean(), + found: cond_type, + context: TypeMismatchContext::Condition, + location: if_statement.location, + }); + } + } else { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), - found: condition_type.unwrap_or_default(), + found: TypeInfo::default(), context: TypeMismatchContext::Condition, location: if_statement.location, }); @@ -703,15 +718,18 @@ impl TypeChecker { let mut expr_ref = initial_value.borrow_mut(); if let Expression::Uzumaki(uzumaki_rc) = &mut *expr_ref { ctx.set_node_typeinfo(uzumaki_rc.id, target_type.clone()); - } else if let Some(init_type) = self.infer_expression(&expr_ref, ctx) - && init_type != TypeInfo::new(&variable_definition_statement.ty) - { - self.errors.push(TypeCheckError::TypeMismatch { - expected: target_type.clone(), - found: init_type, - context: TypeMismatchContext::VariableDefinition, - location: variable_definition_statement.location, - }); + } else if let Some(init_type) = self.infer_expression(&expr_ref, ctx) { + if !target_type.is_error() + && !init_type.is_error() + && init_type != TypeInfo::new(&variable_definition_statement.ty) + { + self.errors.push(TypeCheckError::TypeMismatch { + expected: target_type.clone(), + found: init_type, + context: TypeMismatchContext::VariableDefinition, + location: variable_definition_statement.location, + }); + } } } if let Err(err) = self.symbol_table.push_variable_to_scope( @@ -745,12 +763,19 @@ impl TypeChecker { Statement::Assert(assert_statement) => { let condition_type = self.infer_expression(&assert_statement.expression.borrow(), ctx); - if condition_type.is_none() - || condition_type.as_ref().unwrap().kind != TypeInfoKind::Bool - { + if let Some(cond_type) = condition_type { + if !cond_type.is_error() && !cond_type.is_bool() { + self.errors.push(TypeCheckError::TypeMismatch { + expected: TypeInfo::boolean(), + found: cond_type, + context: TypeMismatchContext::Condition, + location: assert_statement.location, + }); + } + } else { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), - found: condition_type.unwrap_or_default(), + found: TypeInfo::default(), context: TypeMismatchContext::Condition, location: assert_statement.location, }); @@ -790,12 +815,13 @@ impl TypeChecker { { if let Some(index_type) = self.infer_expression(&array_index_access_expression.index.borrow(), ctx) - && !index_type.is_number() { - self.errors.push(TypeCheckError::ArrayIndexNotNumeric { - found: index_type, - location: array_index_access_expression.location, - }); + if !index_type.is_error() && !index_type.is_number() { + self.errors.push(TypeCheckError::ArrayIndexNotNumeric { + found: index_type, + location: array_index_access_expression.location, + }); + } } match &array_type.kind { TypeInfoKind::Array(element_type, _) => { @@ -805,12 +831,16 @@ impl TypeChecker { ); Some((**element_type).clone()) } + TypeInfoKind::Error => Some(array_type.clone()), _ => { self.errors.push(TypeCheckError::ExpectedArrayType { found: array_type, location: array_index_access_expression.location, }); - None + Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }) } } } else { @@ -823,6 +853,10 @@ impl TypeChecker { } else if let Some(object_type) = self.infer_expression(&member_access_expression.expression.borrow(), ctx) { + if object_type.is_error() { + ctx.set_node_typeinfo(member_access_expression.id, object_type.clone()); + return Some(object_type); + } let struct_name = match &object_type.kind { TypeInfoKind::Struct(name) => Some(name.clone()), TypeInfoKind::Custom(name) => { @@ -862,7 +896,10 @@ impl TypeChecker { field_name: field_name.clone(), location: member_access_expression.location, }); - None + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); } } else { self.errors.push(TypeCheckError::FieldNotFound { @@ -870,14 +907,20 @@ impl TypeChecker { field_name: field_name.clone(), location: member_access_expression.location, }); - None + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); } } else { self.errors.push(TypeCheckError::ExpectedStructType { found: object_type, location: member_access_expression.location, }); - None + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); } } else { None @@ -902,7 +945,10 @@ impl TypeChecker { found: TypeInfo::new(ty), location: type_member_access_expression.location, }); - return None; + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); } } } @@ -916,12 +962,16 @@ impl TypeChecker { ) { match &expr_type.kind { TypeInfoKind::Enum(name) => name.clone(), + TypeInfoKind::Error => return Some(expr_type.clone()), _ => { self.errors.push(TypeCheckError::ExpectedEnumType { found: expr_type, location: type_member_access_expression.location, }); - return None; + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); } } } else { @@ -957,14 +1007,20 @@ impl TypeChecker { variant_name: variant_name.clone(), location: type_member_access_expression.location, }); - None + Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }) } } else { self.push_error_dedup(TypeCheckError::UndefinedEnum { name: enum_name, location: type_member_access_expression.location, }); - None + Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }) } } Expression::FunctionCall(function_call_expression) => { @@ -1078,6 +1134,9 @@ impl TypeChecker { self.infer_expression(&member_access.expression.borrow(), ctx); if let Some(receiver_type) = receiver_type { + if receiver_type.is_error() { + return Some(receiver_type); + } let type_name = match &receiver_type.kind { TypeInfoKind::Struct(name) => Some(name.clone()), TypeInfoKind::Custom(name) => { @@ -1162,7 +1221,10 @@ impl TypeChecker { method_name: method_name.clone(), location: member_access.location, }); - return None; + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); } self.errors.push(TypeCheckError::MethodCallOnNonStruct { found: receiver_type, @@ -1174,7 +1236,10 @@ impl TypeChecker { self.infer_expression(&arg.1.borrow(), ctx); } } - return None; + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); } // Receiver type inference failed; infer arguments for better error recovery if let Some(arguments) = &function_call_expression.arguments { @@ -1182,7 +1247,10 @@ impl TypeChecker { self.infer_expression(&arg.1.borrow(), ctx); } } - return None; + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); } let signature = if let Some(s) = self @@ -1209,7 +1277,10 @@ impl TypeChecker { self.infer_expression(&arg.1.borrow(), ctx); } } - return None; + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); }; if let Some(arguments) = &function_call_expression.arguments && arguments.len() != signature.param_types.len() @@ -1224,7 +1295,10 @@ impl TypeChecker { for arg in arguments { self.infer_expression(&arg.1.borrow(), ctx); } - return None; + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); } // Build substitution map for generic functions @@ -1306,7 +1380,12 @@ impl TypeChecker { name: struct_expression.name(), location: struct_expression.location, }); - None + let error_type = TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }; + ctx.set_node_typeinfo(struct_expression.id, error_type.clone()); + Some(error_type) } Expression::PrefixUnary(prefix_unary_expression) => { match prefix_unary_expression.operator { @@ -1314,7 +1393,7 @@ impl TypeChecker { let expression_type_op = self .infer_expression(&prefix_unary_expression.expression.borrow(), ctx); if let Some(expression_type) = expression_type_op { - if expression_type.is_bool() { + if expression_type.is_bool() || expression_type.is_error() { ctx.set_node_typeinfo( prefix_unary_expression.id, expression_type.clone(), @@ -1334,7 +1413,7 @@ impl TypeChecker { let expression_type_op = self .infer_expression(&prefix_unary_expression.expression.borrow(), ctx); if let Some(expression_type) = expression_type_op { - if expression_type.is_signed_integer() { + if expression_type.is_signed_integer() || expression_type.is_error() { ctx.set_node_typeinfo( prefix_unary_expression.id, expression_type.clone(), @@ -1354,7 +1433,7 @@ impl TypeChecker { let expression_type_op = self .infer_expression(&prefix_unary_expression.expression.borrow(), ctx); if let Some(expression_type) = expression_type_op { - if expression_type.is_number() { + if expression_type.is_number() || expression_type.is_error() { ctx.set_node_typeinfo( prefix_unary_expression.id, expression_type.clone(), @@ -1387,7 +1466,7 @@ impl TypeChecker { let left_type = self.infer_expression(&binary_expression.left.borrow(), ctx); let right_type = self.infer_expression(&binary_expression.right.borrow(), ctx); if let (Some(left_type), Some(right_type)) = (left_type, right_type) { - if left_type != right_type { + if !left_type.is_error() && !right_type.is_error() && left_type != right_type { self.errors.push(TypeCheckError::BinaryOperandTypeMismatch { operator: binary_expression.operator.clone(), left: left_type.clone(), @@ -1410,7 +1489,10 @@ impl TypeChecker { found_types: (left_type, right_type), location: binary_expression.location, }); - return None; + return Some(TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }); } } OperatorKind::Eq @@ -1471,14 +1553,17 @@ impl TypeChecker { { for element in &elements[1..] { let element_type = self.infer_expression(&element.borrow(), ctx); - if let Some(element_type) = element_type - && element_type != element_type_info - { - self.errors.push(TypeCheckError::ArrayElementTypeMismatch { - expected: element_type_info.clone(), - found: element_type, - location: array_literal.location, - }); + if let Some(element_type) = element_type { + if !element_type.is_error() + && !element_type_info.is_error() + && element_type != element_type_info + { + self.errors.push(TypeCheckError::ArrayElementTypeMismatch { + expected: element_type_info.clone(), + found: element_type, + location: array_literal.location, + }); + } } } let array_type = TypeInfo { @@ -1526,7 +1611,12 @@ impl TypeChecker { name: identifier.name.clone(), location: identifier.location, }); - None + let error_type = TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }; + ctx.set_node_typeinfo(identifier.id, error_type.clone()); + Some(error_type) } } Expression::Type(type_expr) => { diff --git a/core/type-checker/src/type_info.rs b/core/type-checker/src/type_info.rs index 222dd76..08161e6 100644 --- a/core/type-checker/src/type_info.rs +++ b/core/type-checker/src/type_info.rs @@ -111,6 +111,7 @@ pub enum TypeInfoKind { Struct(String), Enum(String), Spec(String), + Error, } impl Display for TypeInfoKind { @@ -129,6 +130,7 @@ impl Display for TypeInfoKind { | TypeInfoKind::Qualified(ty) | TypeInfoKind::Function(ty) => write!(f, "{ty}"), TypeInfoKind::Generic(ty) => write!(f, "{ty}'"), + TypeInfoKind::Error => write!(f, "{{unknown}}"), } } } @@ -343,6 +345,11 @@ impl TypeInfo { matches!(self.kind, TypeInfoKind::Generic(_)) } + #[must_use] + pub fn is_error(&self) -> bool { + matches!(self.kind, TypeInfoKind::Error) + } + /// Returns true if this is a signed integer type (i8, i16, i32, i64). #[must_use = "this is a pure check with no side effects"] pub fn is_signed_integer(&self) -> bool { @@ -386,7 +393,8 @@ impl TypeInfo { | TypeInfoKind::Function(_) | TypeInfoKind::Struct(_) | TypeInfoKind::Enum(_) - | TypeInfoKind::Spec(_) => self.clone(), + | TypeInfoKind::Spec(_) + | TypeInfoKind::Error => self.clone(), } } @@ -407,7 +415,8 @@ impl TypeInfo { | TypeInfoKind::Function(_) | TypeInfoKind::Struct(_) | TypeInfoKind::Enum(_) - | TypeInfoKind::Spec(_) => false, + | TypeInfoKind::Spec(_) + | TypeInfoKind::Error => false, } } From 4690dc06900451d212662506b470101a97660274 Mon Sep 17 00:00:00 2001 From: Chibuikem Michael Ilonze Date: Fri, 23 Jan 2026 06:10:56 +0100 Subject: [PATCH 2/2] test: Add comprehensive tests for TypeInfoKind::Error (error type poisoning) - Add tests for TypeInfoKind::Error variant behavior (is_error, Display, substitute, has_unresolved_params) - Add tests for cascading error suppression with undeclared variables, undefined structs/functions - Add tests for error propagation through assignments, member access, method calls, function arguments - Add tests for error types in complex expressions (binary ops, arrays, conditionals) - Register error_type_poisoning_tests module in mod.rs --- .../error_type_poisoning_tests.rs | 471 ++++++++++++++++++ tests/src/type_checker/mod.rs | 1 + 2 files changed, 472 insertions(+) create mode 100644 tests/src/type_checker/error_type_poisoning_tests.rs diff --git a/tests/src/type_checker/error_type_poisoning_tests.rs b/tests/src/type_checker/error_type_poisoning_tests.rs new file mode 100644 index 0000000..fb2ecad --- /dev/null +++ b/tests/src/type_checker/error_type_poisoning_tests.rs @@ -0,0 +1,471 @@ +//! Tests for TypeInfoKind::Error (Error Type Poisoning) +//! +//! These tests verify that the type checker: +//! 1. Uses TypeInfoKind::Error to represent failed type lookups +//! 2. Suppresses cascading errors when operating on Error types +//! 3. Propagates Error types through expressions without spurious errors +//! 4. Continues type checking gracefully after encountering Error types +//! +//! The error poisoning feature is inspired by rustc's TyKind::Error model. + +#[cfg(test)] +mod error_type_poisoning_tests { + use crate::utils::build_ast; + use inference_type_checker::type_info::{TypeInfo, TypeInfoKind}; + use inference_type_checker::TypeCheckerBuilder; + + fn try_type_check( + source: &str, + ) -> anyhow::Result { + let arena = build_ast(source.to_string()); + Ok(TypeCheckerBuilder::build_typed_context(arena)?.typed_context()) + } + + mod type_info_error_variant { + use super::*; + + #[test] + fn test_error_type_is_error() { + let error_type = TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }; + assert!(error_type.is_error()); + } + + #[test] + fn test_non_error_types_are_not_error() { + let types = vec![ + TypeInfo::boolean(), + TypeInfo::string(), + TypeInfo::default(), + TypeInfo { + kind: TypeInfoKind::Struct("Point".to_string()), + type_params: vec![], + }, + ]; + for ty in types { + assert!( + !ty.is_error(), + "Expected {:?} to not be an error type", + ty.kind + ); + } + } + + #[test] + fn test_error_type_display() { + let error_type = TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }; + assert_eq!(error_type.to_string(), "{unknown}"); + } + + #[test] + fn test_error_type_has_no_unresolved_params() { + let error_type = TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }; + assert!(!error_type.has_unresolved_params()); + } + + #[test] + fn test_error_type_substitute_unchanged() { + use rustc_hash::FxHashMap; + + let error_type = TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }; + let mut subs = FxHashMap::default(); + subs.insert("T".to_string(), TypeInfo::boolean()); + + let result = error_type.substitute(&subs); + assert!(result.is_error()); + } + } + + mod cascading_error_suppression { + use super::*; + + #[test] + fn test_undeclared_variable_no_cascading_type_mismatch() { + // When a variable is undeclared, we should NOT also get a type mismatch error + let source = r#"fn test() -> i32 { let x: i32 = unknown_var; return x; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_var"), + "Should report undeclared variable: {}", + error_msg + ); + // Should NOT have type mismatch between i32 and {unknown} + let type_mismatch_unknown = + error_msg.contains("type mismatch") && error_msg.contains("{unknown}"); + assert!( + !type_mismatch_unknown, + "Should NOT report type mismatch with {{unknown}}: {}", + error_msg + ); + } + } + + #[test] + fn test_undefined_struct_no_cascading_field_access_errors() { + // When a struct type is unknown, field access should not produce additional errors + let source = r#"fn test(s: UndefinedStruct) -> i32 { return s.field; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undefined struct"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("UndefinedStruct") || error_msg.contains("unknown type"), + "Should report undefined struct: {}", + error_msg + ); + // The field access should not produce a separate "expected struct type" error + // because the Error type prevents this cascade + } + } + + #[test] + fn test_undefined_function_no_cascading_return_type_errors() { + // When a function is undefined, we should not get cascading return type mismatch + let source = r#"fn test() -> i32 { return undefined_func(); }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undefined function"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("undefined_func"), + "Should report undefined function: {}", + error_msg + ); + // Should NOT have type mismatch between i32 and {unknown} + let type_mismatch_unknown = + error_msg.contains("type mismatch") && error_msg.contains("{unknown}"); + assert!( + !type_mismatch_unknown, + "Should NOT report type mismatch with {{unknown}}: {}", + error_msg + ); + } + } + + #[test] + fn test_binary_operation_with_error_type_no_cascading() { + // Binary operation with one undeclared operand should not cause cascading errors + let source = r#"fn test() -> i32 { return unknown_var + 10; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_var"), + "Should report undeclared variable: {}", + error_msg + ); + // Should NOT report "expected numeric type" for the binary operation + // because the left operand is Error type + } + } + + #[test] + fn test_array_index_with_error_array_no_cascading() { + // Array indexing with undeclared array should not cause cascading errors + let source = r#"fn test() -> i32 { return unknown_arr[0]; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_arr"), + "Should report undeclared variable: {}", + error_msg + ); + // Should NOT report "expected array type" for the indexing + // because the array expression is Error type + } + } + + #[test] + fn test_unary_operation_with_error_type_no_cascading() { + // Unary operation on undeclared variable should not cascade + let source = r#"fn test() -> i32 { return -unknown_var; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_var"), + "Should report undeclared variable: {}", + error_msg + ); + } + } + + #[test] + fn test_if_condition_with_error_type_no_cascading() { + // If condition using undeclared variable should not cascade + let source = r#"fn test() -> i32 { if unknown_cond { return 1; } return 0; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_cond"), + "Should report undeclared variable: {}", + error_msg + ); + // Should NOT report "expected bool type" for the condition + // because the condition is Error type + let type_mismatch_unknown = + error_msg.contains("type mismatch") && error_msg.contains("{unknown}"); + assert!( + !type_mismatch_unknown, + "Should NOT report type mismatch with {{unknown}}: {}", + error_msg + ); + } + } + + #[test] + fn test_loop_condition_with_error_type_no_cascading() { + // Loop condition using undeclared variable should not cascade + let source = r#"fn test() -> i32 { loop unknown_cond { break; } return 0; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_cond"), + "Should report undeclared variable: {}", + error_msg + ); + } + } + } + + mod error_propagation { + use super::*; + + #[test] + fn test_error_propagates_through_assignment() { + // Error type on RHS should not produce type mismatch with LHS + let source = + r#"fn test() -> i32 { let x: i32 = unknown_var; let y: bool = x; return 0; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect errors"); + + if let Err(error) = result { + let error_msg = error.to_string(); + // The first error is undeclared variable + assert!( + error_msg.contains("unknown_var"), + "Should report undeclared variable: {}", + error_msg + ); + } + } + + #[test] + fn test_error_in_struct_initialization() { + // Using undefined struct should yield Error type and not cascade + let source = r#"fn test() -> i32 { let p: UndefinedStruct = UndefinedStruct { x: 10 }; return 0; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undefined struct"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("UndefinedStruct"), + "Should report undefined struct: {}", + error_msg + ); + } + } + + #[test] + fn test_chained_member_access_with_error() { + // Chained member access starting with undefined should not cascade + let source = r#"fn test() -> i32 { return unknown_var.field1.field2; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_var"), + "Should report undeclared variable: {}", + error_msg + ); + // Should not have multiple cascading field access errors + } + } + + #[test] + fn test_method_call_on_error_type_no_cascade() { + // Method call on undeclared receiver should not produce cascading errors + let source = r#"fn test() -> i32 { return unknown_obj.method(); }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_obj"), + "Should report undeclared variable: {}", + error_msg + ); + } + } + + #[test] + fn test_error_in_function_argument() { + // Passing undefined variable as function argument + let source = r#"fn add(a: i32, b: i32) -> i32 { return a + b; } fn test() -> i32 { return add(unknown_var, 10); }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_var"), + "Should report undeclared variable: {}", + error_msg + ); + } + } + + #[test] + fn test_multiple_error_sources_independent() { + // Multiple independent errors should all be reported + let source = r#"fn test() -> i32 { let x: i32 = unknown1; let y: i32 = unknown2; return x + y; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variables"); + + if let Err(error) = result { + let error_msg = error.to_string(); + // Both undeclared variables should be reported + assert!( + error_msg.contains("unknown1") && error_msg.contains("unknown2"), + "Should report both undeclared variables: {}", + error_msg + ); + } + } + } + + mod error_type_in_complex_expressions { + use super::*; + + #[test] + fn test_error_in_nested_binary_expressions() { + // Nested binary expression with one error should not cascade + let source = r#"fn test() -> i32 { return (unknown_var + 1) * 2; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_var"), + "Should report undeclared variable: {}", + error_msg + ); + } + } + + #[test] + fn test_error_in_array_literal_element() { + // Array literal with undefined element + let source = + r#"fn test() -> i32 { let arr: [i32; 3] = [1, unknown_var, 3]; return arr[0]; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_var"), + "Should report undeclared variable: {}", + error_msg + ); + } + } + + #[test] + fn test_error_in_conditional_expression() { + // Using undefined variable in if arms + let source = r#"fn test() -> i32 { if true { return unknown_var; } return 0; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_var"), + "Should report undeclared variable: {}", + error_msg + ); + } + } + + #[test] + fn test_error_type_in_assert_condition() { + // Assert with undefined variable should not cascade + let source = r#"fn test() -> i32 { assert unknown_cond; return 0; }"#; + let result = try_type_check(source); + assert!(result.is_err(), "Should detect undeclared variable"); + + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("unknown_cond"), + "Should report undeclared variable: {}", + error_msg + ); + } + } + } + + mod error_type_equality { + use super::*; + + #[test] + fn test_error_types_are_equal() { + let error1 = TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }; + let error2 = TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }; + assert_eq!(error1, error2); + } + + #[test] + fn test_error_not_equal_to_other_types() { + let error = TypeInfo { + kind: TypeInfoKind::Error, + type_params: vec![], + }; + assert_ne!(error, TypeInfo::boolean()); + assert_ne!(error, TypeInfo::string()); + assert_ne!(error, TypeInfo::default()); + } + } +} diff --git a/tests/src/type_checker/mod.rs b/tests/src/type_checker/mod.rs index ba49583..b05484f 100644 --- a/tests/src/type_checker/mod.rs +++ b/tests/src/type_checker/mod.rs @@ -5,5 +5,6 @@ mod array_tests; mod associated_functions; mod coverage; mod error_recovery; +mod error_type_poisoning_tests; mod features; mod type_info_tests;