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/6] 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 82de1b2d..d75f9878 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 222dd763..08161e6a 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/6] 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 00000000..fb2ecadf --- /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 ba495837..b05484fe 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; From 3d7ee8f388738707f01e2ad239285e8678c149c7 Mon Sep 17 00:00:00 2001 From: Chibuikem Michael Ilonze Date: Tue, 27 Jan 2026 04:19:52 +0100 Subject: [PATCH 3/6] refactor: add message to TypeInfoKind::Error variant for better tracking --- core/type-checker/src/type_checker.rs | 91 ++++++------------- core/type-checker/src/type_info.rs | 18 +++- .../error_type_poisoning_tests.rs | 40 +++----- 3 files changed, 50 insertions(+), 99 deletions(-) diff --git a/core/type-checker/src/type_checker.rs b/core/type-checker/src/type_checker.rs index d75f9878..58acd873 100644 --- a/core/type-checker/src/type_checker.rs +++ b/core/type-checker/src/type_checker.rs @@ -831,16 +831,13 @@ impl TypeChecker { ); Some((**element_type).clone()) } - TypeInfoKind::Error => Some(array_type.clone()), + TypeInfoKind::Error(_) => Some(array_type.clone()), _ => { self.errors.push(TypeCheckError::ExpectedArrayType { found: array_type, location: array_index_access_expression.location, }); - Some(TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }) + Some(TypeInfo::error("Expected array type")) } } } else { @@ -896,10 +893,7 @@ impl TypeChecker { field_name: field_name.clone(), location: member_access_expression.location, }); - return Some(TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }); + return Some(TypeInfo::error("Field not found")); } } else { self.errors.push(TypeCheckError::FieldNotFound { @@ -907,20 +901,14 @@ impl TypeChecker { field_name: field_name.clone(), location: member_access_expression.location, }); - return Some(TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }); + return Some(TypeInfo::error("Struct not found for field access")); } } else { self.errors.push(TypeCheckError::ExpectedStructType { found: object_type, location: member_access_expression.location, }); - return Some(TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }); + return Some(TypeInfo::error("Expected struct type for member access")); } } else { None @@ -945,10 +933,7 @@ impl TypeChecker { found: TypeInfo::new(ty), location: type_member_access_expression.location, }); - return Some(TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }); + return Some(TypeInfo::error("Expected enum type for variant access")); } } } @@ -962,16 +947,13 @@ impl TypeChecker { ) { match &expr_type.kind { TypeInfoKind::Enum(name) => name.clone(), - TypeInfoKind::Error => return Some(expr_type.clone()), + TypeInfoKind::Error(_) => return Some(expr_type.clone()), _ => { self.errors.push(TypeCheckError::ExpectedEnumType { found: expr_type, location: type_member_access_expression.location, }); - return Some(TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }); + return Some(TypeInfo::error("Expected enum type")); } } } else { @@ -1007,20 +989,14 @@ impl TypeChecker { variant_name: variant_name.clone(), location: type_member_access_expression.location, }); - Some(TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }) + Some(TypeInfo::error("Enum variant not found")) } } else { self.push_error_dedup(TypeCheckError::UndefinedEnum { name: enum_name, location: type_member_access_expression.location, }); - Some(TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }) + Some(TypeInfo::error("Undefined enum")) } } Expression::FunctionCall(function_call_expression) => { @@ -1221,26 +1197,20 @@ impl TypeChecker { method_name: method_name.clone(), location: member_access.location, }); - return Some(TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }); + return Some(TypeInfo::error("Method not found")); } self.errors.push(TypeCheckError::MethodCallOnNonStruct { - found: receiver_type, - location: function_call_expression.location, - }); - // Infer arguments even for non-struct receiver for better error recovery - if let Some(arguments) = &function_call_expression.arguments { - for arg in arguments { - self.infer_expression(&arg.1.borrow(), ctx); + found: receiver_type, + location: function_call_expression.location, + }); + // Infer arguments even for non-struct receiver for better error recovery + if let Some(arguments) = &function_call_expression.arguments { + for arg in arguments { + self.infer_expression(&arg.1.borrow(), ctx); + } } + return Some(TypeInfo::error("Method call on non-struct")); } - 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 { for arg in arguments { @@ -1248,7 +1218,7 @@ impl TypeChecker { } } return Some(TypeInfo { - kind: TypeInfoKind::Error, + kind: TypeInfoKind::Error("Poisoned type from error".to_string()), type_params: vec![], }); } @@ -1278,7 +1248,7 @@ impl TypeChecker { } } return Some(TypeInfo { - kind: TypeInfoKind::Error, + kind: TypeInfoKind::Error("Poisoned type from error".to_string()), type_params: vec![], }); }; @@ -1296,7 +1266,7 @@ impl TypeChecker { self.infer_expression(&arg.1.borrow(), ctx); } return Some(TypeInfo { - kind: TypeInfoKind::Error, + kind: TypeInfoKind::Error("Poisoned type from error".to_string()), type_params: vec![], }); } @@ -1380,10 +1350,7 @@ impl TypeChecker { name: struct_expression.name(), location: struct_expression.location, }); - let error_type = TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }; + let error_type = TypeInfo::error("Undefined struct"); ctx.set_node_typeinfo(struct_expression.id, error_type.clone()); Some(error_type) } @@ -1489,10 +1456,7 @@ impl TypeChecker { found_types: (left_type, right_type), location: binary_expression.location, }); - return Some(TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }); + return Some(TypeInfo::error("Invalid binary operand")); } } OperatorKind::Eq @@ -1611,10 +1575,7 @@ impl TypeChecker { name: identifier.name.clone(), location: identifier.location, }); - let error_type = TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }; + let error_type = TypeInfo::error("Undefined identifier"); ctx.set_node_typeinfo(identifier.id, error_type.clone()); Some(error_type) } diff --git a/core/type-checker/src/type_info.rs b/core/type-checker/src/type_info.rs index 08161e6a..1e8b7f17 100644 --- a/core/type-checker/src/type_info.rs +++ b/core/type-checker/src/type_info.rs @@ -111,7 +111,7 @@ pub enum TypeInfoKind { Struct(String), Enum(String), Spec(String), - Error, + Error(String), } impl Display for TypeInfoKind { @@ -130,7 +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}}"), + TypeInfoKind::Error(msg) => write!(f, "{{unknown: {msg}}}"), } } } @@ -226,6 +226,14 @@ impl TypeInfo { } } + #[must_use] + pub fn error(msg: impl Into) -> Self { + Self { + kind: TypeInfoKind::Error(msg.into()), + type_params: vec![], + } + } + #[must_use] pub fn string() -> Self { Self { @@ -347,7 +355,7 @@ impl TypeInfo { #[must_use] pub fn is_error(&self) -> bool { - matches!(self.kind, TypeInfoKind::Error) + matches!(self.kind, TypeInfoKind::Error(_)) } /// Returns true if this is a signed integer type (i8, i16, i32, i64). @@ -394,7 +402,7 @@ impl TypeInfo { | TypeInfoKind::Struct(_) | TypeInfoKind::Enum(_) | TypeInfoKind::Spec(_) - | TypeInfoKind::Error => self.clone(), + | TypeInfoKind::Error(_) => self.clone(), } } @@ -416,7 +424,7 @@ impl TypeInfo { | TypeInfoKind::Struct(_) | TypeInfoKind::Enum(_) | TypeInfoKind::Spec(_) - | TypeInfoKind::Error => false, + | TypeInfoKind::Error(_) => false, } } diff --git a/tests/src/type_checker/error_type_poisoning_tests.rs b/tests/src/type_checker/error_type_poisoning_tests.rs index fb2ecadf..f77ea651 100644 --- a/tests/src/type_checker/error_type_poisoning_tests.rs +++ b/tests/src/type_checker/error_type_poisoning_tests.rs @@ -26,10 +26,7 @@ mod error_type_poisoning_tests { #[test] fn test_error_type_is_error() { - let error_type = TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }; + let error_type = TypeInfo::error("test error"); assert!(error_type.is_error()); } @@ -55,19 +52,13 @@ mod error_type_poisoning_tests { #[test] fn test_error_type_display() { - let error_type = TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }; - assert_eq!(error_type.to_string(), "{unknown}"); + let error_type = TypeInfo::error("test message"); + assert_eq!(error_type.to_string(), "{unknown: test message}"); } #[test] fn test_error_type_has_no_unresolved_params() { - let error_type = TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }; + let error_type = TypeInfo::error("test error"); assert!(!error_type.has_unresolved_params()); } @@ -75,10 +66,7 @@ mod error_type_poisoning_tests { fn test_error_type_substitute_unchanged() { use rustc_hash::FxHashMap; - let error_type = TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }; + let error_type = TypeInfo::error("test error"); let mut subs = FxHashMap::default(); subs.insert("T".to_string(), TypeInfo::boolean()); @@ -446,23 +434,17 @@ mod error_type_poisoning_tests { #[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![], - }; + let error1 = TypeInfo::error("same message"); + let error2 = TypeInfo::error("same message"); assert_eq!(error1, error2); + + let error3 = TypeInfo::error("different message"); + assert_ne!(error1, error3); } #[test] fn test_error_not_equal_to_other_types() { - let error = TypeInfo { - kind: TypeInfoKind::Error, - type_params: vec![], - }; + let error = TypeInfo::error("test error"); assert_ne!(error, TypeInfo::boolean()); assert_ne!(error, TypeInfo::string()); assert_ne!(error, TypeInfo::default()); From 2c7712047cf87bf5a6aa50c4c4f1a8e5689451fe Mon Sep 17 00:00:00 2001 From: Chibuikem Michael Ilonze Date: Tue, 27 Jan 2026 18:47:13 +0100 Subject: [PATCH 4/6] Refactor: use TypeInfo instead of Option and update error labels --- core/type-checker/src/type_checker.rs | 645 ++++++++---------- core/type-checker/src/type_info.rs | 2 +- .../error_type_poisoning_tests.rs | 18 +- 3 files changed, 305 insertions(+), 360 deletions(-) diff --git a/core/type-checker/src/type_checker.rs b/core/type-checker/src/type_checker.rs index 58acd873..28a5f8e1 100644 --- a/core/type-checker/src/type_checker.rs +++ b/core/type-checker/src/type_checker.rs @@ -597,8 +597,8 @@ impl TypeChecker { let target_type = self.infer_expression(&assign_statement.left.borrow(), ctx); let right_expr = assign_statement.right.borrow(); if let Expression::Uzumaki(uzumaki_rc) = &*right_expr { - if let Some(target) = &target_type { - ctx.set_node_typeinfo(uzumaki_rc.id, target.clone()); + if !target_type.is_error() { + ctx.set_node_typeinfo(uzumaki_rc.id, target_type.clone()); } else { self.errors.push(TypeCheckError::CannotInferUzumakiType { location: uzumaki_rc.location, @@ -606,15 +606,13 @@ impl TypeChecker { } } else { let value_type = self.infer_expression(&right_expr, ctx); - 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, - }); - } + if !target_type.is_error() && !value_type.is_error() && target_type != value_type { + self.errors.push(TypeCheckError::TypeMismatch { + expected: target_type, + found: value_type, + context: TypeMismatchContext::Assignment, + location: assign_statement.location, + }); } } } @@ -638,9 +636,8 @@ impl TypeChecker { return_type.clone(), ); } else { - let value_type = + let val = self.infer_expression(&return_statement.expression.borrow(), ctx); - 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(), @@ -653,9 +650,9 @@ impl TypeChecker { } Statement::Loop(loop_statement) => { if let Some(condition) = &*loop_statement.condition.borrow() { - let condition_type = self.infer_expression(condition, ctx); - if let Some(cond_type) = condition_type { - if !cond_type.is_error() && !cond_type.is_bool() { + let cond_type = self.infer_expression(condition, ctx); + if !cond_type.is_error() { + if !cond_type.is_bool() { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), found: cond_type, @@ -680,9 +677,9 @@ impl TypeChecker { } Statement::Break(_) => {} Statement::If(if_statement) => { - let condition_type = self.infer_expression(&if_statement.condition.borrow(), ctx); - if let Some(cond_type) = condition_type { - if !cond_type.is_error() && !cond_type.is_bool() { + let cond_type = self.infer_expression(&if_statement.condition.borrow(), ctx); + if !cond_type.is_error() { + if !cond_type.is_bool() { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), found: cond_type, @@ -718,7 +715,8 @@ 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) { + } else { + let 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) @@ -761,10 +759,10 @@ impl TypeChecker { } } Statement::Assert(assert_statement) => { - let condition_type = + let cond_type = self.infer_expression(&assert_statement.expression.borrow(), ctx); - if let Some(cond_type) = condition_type { - if !cond_type.is_error() && !cond_type.is_bool() { + if !cond_type.is_error() { + if !cond_type.is_bool() { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), found: cond_type, @@ -805,54 +803,50 @@ impl TypeChecker { &mut self, expression: &Expression, ctx: &mut TypedContext, - ) -> Option { + ) -> TypeInfo { match expression { Expression::ArrayIndexAccess(array_index_access_expression) => { if let Some(type_info) = ctx.get_node_typeinfo(array_index_access_expression.id) { - Some(type_info.clone()) - } else if let Some(array_type) = - self.infer_expression(&array_index_access_expression.array.borrow(), ctx) - { - if let Some(index_type) = - self.infer_expression(&array_index_access_expression.index.borrow(), ctx) - { - if !index_type.is_error() && !index_type.is_number() { - self.errors.push(TypeCheckError::ArrayIndexNotNumeric { - found: index_type, - location: array_index_access_expression.location, - }); - } + type_info.clone() + } else { + let array_type = self.infer_expression(&array_index_access_expression.array.borrow(), ctx); + let index_type = self.infer_expression(&array_index_access_expression.index.borrow(), ctx); + + 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, _) => { + let element_type = (**element_type).clone(); ctx.set_node_typeinfo( array_index_access_expression.id, - (**element_type).clone(), + element_type.clone(), ); - Some((**element_type).clone()) + element_type } - TypeInfoKind::Error(_) => Some(array_type.clone()), + TypeInfoKind::Error(_) => array_type.clone(), _ => { self.errors.push(TypeCheckError::ExpectedArrayType { found: array_type, location: array_index_access_expression.location, }); - Some(TypeInfo::error("Expected array type")) + TypeInfo::error("Expected array type") } } - } else { - None } } Expression::MemberAccess(member_access_expression) => { if let Some(type_info) = ctx.get_node_typeinfo(member_access_expression.id) { - Some(type_info.clone()) - } else if let Some(object_type) = - self.infer_expression(&member_access_expression.expression.borrow(), ctx) - { + type_info.clone() + } else { + let 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); + return object_type; } let struct_name = match &object_type.kind { TypeInfoKind::Struct(name) => Some(name.clone()), @@ -886,14 +880,14 @@ impl TypeChecker { member_access_expression.id, field_type.clone(), ); - Some(field_type) + field_type } else { self.errors.push(TypeCheckError::FieldNotFound { struct_name, field_name: field_name.clone(), location: member_access_expression.location, }); - return Some(TypeInfo::error("Field not found")); + TypeInfo::error("Field not found") } } else { self.errors.push(TypeCheckError::FieldNotFound { @@ -901,63 +895,61 @@ impl TypeChecker { field_name: field_name.clone(), location: member_access_expression.location, }); - return Some(TypeInfo::error("Struct not found for field access")); + TypeInfo::error("Struct not found for field access") } } else { self.errors.push(TypeCheckError::ExpectedStructType { found: object_type, location: member_access_expression.location, }); - return Some(TypeInfo::error("Expected struct type for member access")); + TypeInfo::error("Expected struct type for member access") } - } else { - None } } Expression::TypeMemberAccess(type_member_access_expression) => { if let Some(type_info) = ctx.get_node_typeinfo(type_member_access_expression.id) { - return Some(type_info.clone()); + return type_info.clone(); } - let inner_expr = type_member_access_expression.expression.borrow(); - - // Extract enum name from the expression - handle Type enum properly - let enum_name = match &*inner_expr { - Expression::Type(ty) => { - // Type enum does NOT have a .name() method - must match variants - match ty { - Type::Custom(ident) => ident.name.clone(), - _ => { - // Simple, Array, Generic, Function, QualifiedName, Qualified are not valid for enum access - self.errors.push(TypeCheckError::ExpectedEnumType { - found: TypeInfo::new(ty), - location: type_member_access_expression.location, - }); - return Some(TypeInfo::error("Expected enum type for variant access")); + let enum_name = { + let inner_expr = type_member_access_expression.expression.borrow(); + + // Extract enum name from the expression - handle Type enum properly + match &*inner_expr { + Expression::Type(ty) => { + // Type enum does NOT have a .name() method - must match variants + match ty { + Type::Custom(ident) => ident.name.clone(), + _ => { + // Simple, Array, Generic, Function, QualifiedName, Qualified are not valid for enum access + self.errors.push(TypeCheckError::ExpectedEnumType { + found: TypeInfo::new(ty), + location: type_member_access_expression.location, + }); + return TypeInfo::error("Expected enum type for variant access"); + } } } - } - Expression::Identifier(id) => id.name.clone(), - _ => { - // For other expressions, try to infer the type - drop(inner_expr); // Release borrow before mutable borrow - if let Some(expr_type) = self.infer_expression( - &type_member_access_expression.expression.borrow(), - ctx, - ) { + Expression::Identifier(id) => id.name.clone(), + _ => { + // For other expressions, try to infer the type + drop(inner_expr); // Release borrow before recursion + let expr_type = self.infer_expression( + &type_member_access_expression.expression.borrow(), + ctx, + ); + match &expr_type.kind { TypeInfoKind::Enum(name) => name.clone(), - TypeInfoKind::Error(_) => return Some(expr_type.clone()), + TypeInfoKind::Error(_) => return expr_type, _ => { self.errors.push(TypeCheckError::ExpectedEnumType { found: expr_type, location: type_member_access_expression.location, }); - return Some(TypeInfo::error("Expected enum type")); + return TypeInfo::error("Expected enum type"); } } - } else { - return None; } } }; @@ -967,8 +959,7 @@ impl TypeChecker { // Look up the enum and validate variant if let Some(enum_info) = self.symbol_table.lookup_enum(&enum_name) { if enum_info.variants.contains(variant_name) { - // Check enum visibility (variants inherit the enum's visibility, - // unlike struct fields which have per-field visibility) + // Check enum visibility (variants inherit the enum's visibility) self.check_and_report_visibility( &enum_info.visibility, enum_info.definition_scope_id, @@ -982,21 +973,21 @@ impl TypeChecker { type_params: vec![], }; ctx.set_node_typeinfo(type_member_access_expression.id, enum_type.clone()); - Some(enum_type) + enum_type } else { self.errors.push(TypeCheckError::VariantNotFound { enum_name, variant_name: variant_name.clone(), location: type_member_access_expression.location, }); - Some(TypeInfo::error("Enum variant not found")) + TypeInfo::error("Enum variant not found") } } else { self.push_error_dedup(TypeCheckError::UndefinedEnum { name: enum_name, location: type_member_access_expression.location, }); - Some(TypeInfo::error("Undefined enum")) + TypeInfo::error("Undefined enum") } } Expression::FunctionCall(function_call_expression) => { @@ -1097,7 +1088,7 @@ impl TypeChecker { function_call_expression.id, signature.return_type.clone(), ); - return Some(signature.return_type.clone()); + return signature.return_type.clone(); } // Not an enum and not a method - fall through to standard function handling } @@ -1106,121 +1097,107 @@ impl TypeChecker { if let Expression::MemberAccess(member_access) = &function_call_expression.function { - let receiver_type = - self.infer_expression(&member_access.expression.borrow(), ctx); + let receiver_type = 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) => { - if self.symbol_table.lookup_struct(name).is_some() { - Some(name.clone()) - } else { - None - } + if receiver_type.is_error() { + return receiver_type; + } + let type_name = match &receiver_type.kind { + TypeInfoKind::Struct(name) => Some(name.clone()), + TypeInfoKind::Custom(name) => { + if self.symbol_table.lookup_struct(name).is_some() { + Some(name.clone()) + } else { + None } - _ => None, - }; - - if let Some(type_name) = type_name { - let method_name = &member_access.name.name; - if let Some(method_info) = - self.symbol_table.lookup_method(&type_name, method_name) - { - // Check if this is an associated function being called as instance method - if !method_info.is_instance_method() { - // Error: calling associated function with receiver - self.errors.push( - TypeCheckError::AssociatedFunctionCalledAsMethod { - type_name: type_name.clone(), - method_name: method_name.clone(), - location: member_access.location, - }, - ); - // Continue with type checking for better error recovery - } + } + _ => None, + }; - // Check visibility of the method - self.check_and_report_visibility( - &method_info.visibility, - method_info.scope_id, - &member_access.location, - VisibilityContext::Method { + if let Some(type_name) = type_name { + let method_name = &member_access.name.name; + if let Some(method_info) = + self.symbol_table.lookup_method(&type_name, method_name) + { + // Check if this is an associated function being called as instance method + if !method_info.is_instance_method() { + // Error: calling associated function with receiver + self.errors.push( + TypeCheckError::AssociatedFunctionCalledAsMethod { type_name: type_name.clone(), method_name: method_name.clone(), + location: member_access.location, }, ); + } - let signature = &method_info.signature; - let arg_count = function_call_expression - .arguments - .as_ref() - .map_or(0, Vec::len); - - if arg_count != signature.param_types.len() { - self.errors.push(TypeCheckError::ArgumentCountMismatch { - kind: "method", - name: format!("{}::{}", type_name, method_name), - expected: signature.param_types.len(), - found: arg_count, - location: function_call_expression.location, - }); - } + // Check visibility of the method + self.check_and_report_visibility( + &method_info.visibility, + method_info.scope_id, + &member_access.location, + VisibilityContext::Method { + type_name: type_name.clone(), + method_name: method_name.clone(), + }, + ); - if let Some(arguments) = &function_call_expression.arguments { - for arg in arguments { - self.infer_expression(&arg.1.borrow(), ctx); - } - } + let signature = &method_info.signature; + let arg_count = function_call_expression + .arguments + .as_ref() + .map_or(0, Vec::len); - ctx.set_node_typeinfo( - member_access.id, - TypeInfo { - kind: TypeInfoKind::Function(format!( - "{}::{}", - type_name, method_name - )), - type_params: vec![], - }, - ); - ctx.set_node_typeinfo( - function_call_expression.id, - signature.return_type.clone(), - ); - return Some(signature.return_type.clone()); + if arg_count != signature.param_types.len() { + self.errors.push(TypeCheckError::ArgumentCountMismatch { + kind: "method", + name: format!("{}::{}", type_name, method_name), + expected: signature.param_types.len(), + found: arg_count, + location: function_call_expression.location, + }); } - self.errors.push(TypeCheckError::MethodNotFound { - type_name, - method_name: method_name.clone(), - location: member_access.location, - }); - return Some(TypeInfo::error("Method not found")); - } - self.errors.push(TypeCheckError::MethodCallOnNonStruct { - found: receiver_type, - location: function_call_expression.location, - }); - // Infer arguments even for non-struct receiver for better error recovery + if let Some(arguments) = &function_call_expression.arguments { for arg in arguments { self.infer_expression(&arg.1.borrow(), ctx); } } - return Some(TypeInfo::error("Method call on non-struct")); + + ctx.set_node_typeinfo( + member_access.id, + TypeInfo { + kind: TypeInfoKind::Function(format!( + "{}::{}", + type_name, method_name + )), + type_params: vec![], + }, + ); + ctx.set_node_typeinfo( + function_call_expression.id, + signature.return_type.clone(), + ); + return signature.return_type.clone(); } - // Receiver type inference failed; infer arguments for better error recovery + self.errors.push(TypeCheckError::MethodNotFound { + type_name, + method_name: method_name.clone(), + location: member_access.location, + }); + return TypeInfo::error("Method not found"); + } + self.errors.push(TypeCheckError::MethodCallOnNonStruct { + found: receiver_type, + location: function_call_expression.location, + }); + // Infer arguments even for non-struct receiver for better error recovery if let Some(arguments) = &function_call_expression.arguments { for arg in arguments { self.infer_expression(&arg.1.borrow(), ctx); } } - return Some(TypeInfo { - kind: TypeInfoKind::Error("Poisoned type from error".to_string()), - type_params: vec![], - }); + return TypeInfo::error("Method call on non-struct"); } let signature = if let Some(s) = self @@ -1242,15 +1219,7 @@ impl TypeChecker { name: function_call_expression.name(), location: function_call_expression.location, }); - if let Some(arguments) = &function_call_expression.arguments { - for arg in arguments { - self.infer_expression(&arg.1.borrow(), ctx); - } - } - return Some(TypeInfo { - kind: TypeInfoKind::Error("Poisoned type from error".to_string()), - type_params: vec![], - }); + return TypeInfo::error("Poisoned type from error"); }; if let Some(arguments) = &function_call_expression.arguments && arguments.len() != signature.param_types.len() @@ -1265,10 +1234,7 @@ impl TypeChecker { for arg in arguments { self.infer_expression(&arg.1.borrow(), ctx); } - return Some(TypeInfo { - kind: TypeInfoKind::Error("Poisoned type from error".to_string()), - type_params: vec![], - }); + return TypeInfo::error("Poisoned type from error"); } // Build substitution map for generic functions @@ -1335,16 +1301,16 @@ impl TypeChecker { } ctx.set_node_typeinfo(function_call_expression.id, return_type.clone()); - Some(return_type) + return_type } Expression::Struct(struct_expression) => { if let Some(type_info) = ctx.get_node_typeinfo(struct_expression.id) { - return Some(type_info.clone()); + return type_info.clone(); } let struct_type = self.symbol_table.lookup_type(&struct_expression.name()); if let Some(struct_type) = struct_type { ctx.set_node_typeinfo(struct_expression.id, struct_type.clone()); - return Some(struct_type); + return struct_type; } self.push_error_dedup(TypeCheckError::UndefinedStruct { name: struct_expression.name(), @@ -1352,182 +1318,161 @@ impl TypeChecker { }); let error_type = TypeInfo::error("Undefined struct"); ctx.set_node_typeinfo(struct_expression.id, error_type.clone()); - Some(error_type) + error_type } Expression::PrefixUnary(prefix_unary_expression) => { match prefix_unary_expression.operator { UnaryOperatorKind::Not => { - let expression_type_op = self + let expression_type = self .infer_expression(&prefix_unary_expression.expression.borrow(), ctx); - if let Some(expression_type) = expression_type_op { - if expression_type.is_bool() || expression_type.is_error() { - ctx.set_node_typeinfo( - prefix_unary_expression.id, - expression_type.clone(), - ); - return Some(expression_type); - } - self.errors.push(TypeCheckError::InvalidUnaryOperand { - operator: UnaryOperatorKind::Not, - expected_type: "booleans", - found_type: expression_type, - location: prefix_unary_expression.location, - }); + if expression_type.is_bool() || expression_type.is_error() { + ctx.set_node_typeinfo( + prefix_unary_expression.id, + expression_type.clone(), + ); + return expression_type; } - None + self.errors.push(TypeCheckError::InvalidUnaryOperand { + operator: UnaryOperatorKind::Not, + expected_type: "booleans", + found_type: expression_type, + location: prefix_unary_expression.location, + }); + TypeInfo::error("Invalid unary operand") } UnaryOperatorKind::Neg => { - let expression_type_op = self + let expression_type = self .infer_expression(&prefix_unary_expression.expression.borrow(), ctx); - if let Some(expression_type) = expression_type_op { - if expression_type.is_signed_integer() || expression_type.is_error() { - ctx.set_node_typeinfo( - prefix_unary_expression.id, - expression_type.clone(), - ); - return Some(expression_type); - } - self.errors.push(TypeCheckError::InvalidUnaryOperand { - operator: UnaryOperatorKind::Neg, - expected_type: "signed integers (i8, i16, i32, i64)", - found_type: expression_type, - location: prefix_unary_expression.location, - }); + if expression_type.is_signed_integer() || expression_type.is_error() { + ctx.set_node_typeinfo( + prefix_unary_expression.id, + expression_type.clone(), + ); + return expression_type; } - None + self.errors.push(TypeCheckError::InvalidUnaryOperand { + operator: UnaryOperatorKind::Neg, + expected_type: "signed integers (i8, i16, i32, i64)", + found_type: expression_type, + location: prefix_unary_expression.location, + }); + TypeInfo::error("Invalid unary operand") } UnaryOperatorKind::BitNot => { - let expression_type_op = self + let expression_type = self .infer_expression(&prefix_unary_expression.expression.borrow(), ctx); - if let Some(expression_type) = expression_type_op { - if expression_type.is_number() || expression_type.is_error() { - ctx.set_node_typeinfo( - prefix_unary_expression.id, - expression_type.clone(), - ); - return Some(expression_type); - } - self.errors.push(TypeCheckError::InvalidUnaryOperand { - operator: UnaryOperatorKind::BitNot, - expected_type: "integers (i8, i16, i32, i64, u8, u16, u32, u64)", - found_type: expression_type, - location: prefix_unary_expression.location, - }); + if expression_type.is_number() || expression_type.is_error() { + ctx.set_node_typeinfo( + prefix_unary_expression.id, + expression_type.clone(), + ); + return expression_type; } - None + self.errors.push(TypeCheckError::InvalidUnaryOperand { + operator: UnaryOperatorKind::BitNot, + expected_type: "integers (i8, i16, i32, i64, u8, u16, u32, u64)", + found_type: expression_type, + location: prefix_unary_expression.location, + }); + TypeInfo::error("Invalid unary operand") } } } Expression::Parenthesized(parenthesized_expression) => { let inner_type = self.infer_expression(&parenthesized_expression.expression.borrow(), ctx); - if let Some(ref type_info) = inner_type { - ctx.set_node_typeinfo(parenthesized_expression.id, type_info.clone()); - } + ctx.set_node_typeinfo(parenthesized_expression.id, inner_type.clone()); inner_type } Expression::Binary(binary_expression) => { if let Some(type_info) = ctx.get_node_typeinfo(binary_expression.id) { - return Some(type_info.clone()); + return type_info.clone(); } 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.is_error() && !right_type.is_error() && left_type != right_type { - self.errors.push(TypeCheckError::BinaryOperandTypeMismatch { - operator: binary_expression.operator.clone(), - left: left_type.clone(), - right: right_type.clone(), - location: binary_expression.location, - }); + + 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(), + right: right_type.clone(), + location: binary_expression.location, + }); + } + let res_type = match binary_expression.operator { + OperatorKind::And | OperatorKind::Or => { + if left_type.is_bool() && right_type.is_bool() { + TypeInfo::boolean() + } else { + self.errors.push(TypeCheckError::InvalidBinaryOperand { + operator: binary_expression.operator.clone(), + expected_kind: "logical", + operand_desc: "non-boolean types", + found_types: (left_type, right_type), + location: binary_expression.location, + }); + TypeInfo::error("Invalid binary operand") + } } - let res_type = match binary_expression.operator { - OperatorKind::And | OperatorKind::Or => { - if left_type.is_bool() && right_type.is_bool() { - TypeInfo { - kind: TypeInfoKind::Bool, - type_params: vec![], - } - } else { - self.errors.push(TypeCheckError::InvalidBinaryOperand { - operator: binary_expression.operator.clone(), - expected_kind: "logical", - operand_desc: "non-boolean types", - found_types: (left_type, right_type), - location: binary_expression.location, - }); - return Some(TypeInfo::error("Invalid binary operand")); - } + OperatorKind::Eq + | OperatorKind::Ne + | OperatorKind::Lt + | OperatorKind::Le + | OperatorKind::Gt + | OperatorKind::Ge => TypeInfo::boolean(), + OperatorKind::Pow + | OperatorKind::Add + | OperatorKind::Sub + | OperatorKind::Mul + | OperatorKind::Div + | OperatorKind::Mod + | OperatorKind::BitAnd + | OperatorKind::BitOr + | OperatorKind::BitXor + | OperatorKind::BitNot + | OperatorKind::Shl + | OperatorKind::Shr => { + if !left_type.is_number() || !right_type.is_number() { + self.errors.push(TypeCheckError::InvalidBinaryOperand { + operator: binary_expression.operator.clone(), + expected_kind: "arithmetic", + operand_desc: "non-number types", + found_types: (left_type.clone(), right_type.clone()), + location: binary_expression.location, + }); } - OperatorKind::Eq - | OperatorKind::Ne - | OperatorKind::Lt - | OperatorKind::Le - | OperatorKind::Gt - | OperatorKind::Ge => TypeInfo { - kind: TypeInfoKind::Bool, - type_params: vec![], - }, - OperatorKind::Pow - | OperatorKind::Add - | OperatorKind::Sub - | OperatorKind::Mul - | OperatorKind::Div - | OperatorKind::Mod - | OperatorKind::BitAnd - | OperatorKind::BitOr - | OperatorKind::BitXor - | OperatorKind::BitNot - | OperatorKind::Shl - | OperatorKind::Shr => { - if !left_type.is_number() || !right_type.is_number() { - self.errors.push(TypeCheckError::InvalidBinaryOperand { - operator: binary_expression.operator.clone(), - expected_kind: "arithmetic", - operand_desc: "non-number types", - found_types: (left_type.clone(), right_type.clone()), - location: binary_expression.location, - }); - } - if left_type != right_type { - self.errors.push(TypeCheckError::BinaryOperandTypeMismatch { - operator: binary_expression.operator.clone(), - left: left_type.clone(), - right: right_type, - location: binary_expression.location, - }); - } - left_type.clone() + if left_type != right_type { + self.errors.push(TypeCheckError::BinaryOperandTypeMismatch { + operator: binary_expression.operator.clone(), + left: left_type.clone(), + right: right_type, + location: binary_expression.location, + }); } - }; - ctx.set_node_typeinfo(binary_expression.id, res_type.clone()); - Some(res_type) - } else { - None - } + left_type.clone() + } + }; + ctx.set_node_typeinfo(binary_expression.id, res_type.clone()); + res_type } Expression::Literal(literal) => match literal { Literal::Array(array_literal) => { if let Some(type_info) = ctx.get_node_typeinfo(array_literal.id) { - return Some(type_info); + return type_info; } - if let Some(elements) = &array_literal.elements - && let Some(element_type_info) = - self.infer_expression(&elements[0].borrow(), ctx) - { + if let Some(elements) = &array_literal.elements { + let element_type_info = self.infer_expression(&elements[0].borrow(), ctx); for element in &elements[1..] { let element_type = self.infer_expression(&element.borrow(), ctx); - 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, - }); - } + 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 { @@ -1538,38 +1483,38 @@ impl TypeChecker { type_params: vec![], }; ctx.set_node_typeinfo(array_literal.id, array_type.clone()); - return Some(array_type); + return array_type; } - None + TypeInfo::error("Empty array literal") } Literal::Bool(_) => { ctx.set_node_typeinfo(literal.id(), TypeInfo::boolean()); - Some(TypeInfo::boolean()) + TypeInfo::boolean() } Literal::String(sl) => { ctx.set_node_typeinfo(sl.id, TypeInfo::string()); - Some(TypeInfo::string()) + TypeInfo::string() } Literal::Number(number_literal) => { - if ctx.get_node_typeinfo(number_literal.id).is_some() { - return ctx.get_node_typeinfo(number_literal.id); + if let Some(ti) = ctx.get_node_typeinfo(number_literal.id) { + return ti; } let res_type = TypeInfo { kind: TypeInfoKind::Number(NumberType::I32), type_params: vec![], }; ctx.set_node_typeinfo(number_literal.id, res_type.clone()); - Some(res_type) + res_type } Literal::Unit(_) => { ctx.set_node_typeinfo(literal.id(), TypeInfo::default()); - Some(TypeInfo::default()) + TypeInfo::default() } }, Expression::Identifier(identifier) => { if let Some(var_ty) = self.symbol_table.lookup_variable(&identifier.name) { ctx.set_node_typeinfo(identifier.id, var_ty.clone()); - Some(var_ty) + var_ty } else { self.push_error_dedup(TypeCheckError::UnknownIdentifier { name: identifier.name.clone(), @@ -1577,7 +1522,7 @@ impl TypeChecker { }); let error_type = TypeInfo::error("Undefined identifier"); ctx.set_node_typeinfo(identifier.id, error_type.clone()); - Some(error_type) + error_type } } Expression::Type(type_expr) => { @@ -1586,9 +1531,9 @@ impl TypeChecker { if let Type::Array(array_type) = type_expr { self.infer_expression(&array_type.size.clone(), ctx); } - Some(type_info) + type_info } - Expression::Uzumaki(uzumaki) => ctx.get_node_typeinfo(uzumaki.id), + Expression::Uzumaki(uzumaki) => ctx.get_node_typeinfo(uzumaki.id).unwrap_or_else(|| TypeInfo::error("Uzumaki type not inferred")), } } @@ -2094,7 +2039,7 @@ impl TypeChecker { // Infer the argument type let arg_type = self.infer_expression(&args[i].1.borrow(), ctx); - if let Some(arg_type) = arg_type { + if !arg_type.is_error() { // Check for conflicting inference if let Some(existing) = substitutions.get(type_param_name) { if *existing != arg_type { diff --git a/core/type-checker/src/type_info.rs b/core/type-checker/src/type_info.rs index 1e8b7f17..e3a91132 100644 --- a/core/type-checker/src/type_info.rs +++ b/core/type-checker/src/type_info.rs @@ -130,7 +130,7 @@ impl Display for TypeInfoKind { | TypeInfoKind::Qualified(ty) | TypeInfoKind::Function(ty) => write!(f, "{ty}"), TypeInfoKind::Generic(ty) => write!(f, "{ty}'"), - TypeInfoKind::Error(msg) => write!(f, "{{unknown: {msg}}}"), + TypeInfoKind::Error(msg) => write!(f, "{{error: {msg}}}"), } } } diff --git a/tests/src/type_checker/error_type_poisoning_tests.rs b/tests/src/type_checker/error_type_poisoning_tests.rs index f77ea651..1c3dcdf9 100644 --- a/tests/src/type_checker/error_type_poisoning_tests.rs +++ b/tests/src/type_checker/error_type_poisoning_tests.rs @@ -53,7 +53,7 @@ mod error_type_poisoning_tests { #[test] fn test_error_type_display() { let error_type = TypeInfo::error("test message"); - assert_eq!(error_type.to_string(), "{unknown: test message}"); + assert_eq!(error_type.to_string(), "{error: test message}"); } #[test] @@ -92,12 +92,12 @@ mod error_type_poisoning_tests { "Should report undeclared variable: {}", error_msg ); - // Should NOT have type mismatch between i32 and {unknown} + // Should NOT have type mismatch between i32 and {error} let type_mismatch_unknown = - error_msg.contains("type mismatch") && error_msg.contains("{unknown}"); + error_msg.contains("type mismatch") && error_msg.contains("{error}"); assert!( !type_mismatch_unknown, - "Should NOT report type mismatch with {{unknown}}: {}", + "Should NOT report type mismatch with {{error}}: {}", error_msg ); } @@ -136,12 +136,12 @@ mod error_type_poisoning_tests { "Should report undefined function: {}", error_msg ); - // Should NOT have type mismatch between i32 and {unknown} + // Should NOT have type mismatch between i32 and {error} let type_mismatch_unknown = - error_msg.contains("type mismatch") && error_msg.contains("{unknown}"); + error_msg.contains("type mismatch") && error_msg.contains("{error}"); assert!( !type_mismatch_unknown, - "Should NOT report type mismatch with {{unknown}}: {}", + "Should NOT report type mismatch with {{error}}: {}", error_msg ); } @@ -219,10 +219,10 @@ mod error_type_poisoning_tests { // 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}"); + error_msg.contains("type mismatch") && error_msg.contains("{error}"); assert!( !type_mismatch_unknown, - "Should NOT report type mismatch with {{unknown}}: {}", + "Should NOT report type mismatch with {{error}}: {}", error_msg ); } From a9b6d60a4c6ab7f096b430af48cf38a86275380e Mon Sep 17 00:00:00 2001 From: Chibuikem Michael Ilonze Date: Thu, 29 Jan 2026 09:19:12 +0100 Subject: [PATCH 5/6] Fix cascading errors and improve error recovery in TypeChecker - Fixed cascading errors in Loop, If, and Assert statements - Fixed cascading errors in binary logical and arithmetic operators - Deduplicated BinaryOperandTypeMismatch errors - Restored argument inference for undefined function calls - Fixed empty array literal returning Error type - Removed redundant #[cfg(test)] from test file --- core/type-checker/src/type_checker.rs | 134 ++++++++---------- .../error_type_poisoning_tests.rs | 2 +- 2 files changed, 61 insertions(+), 75 deletions(-) diff --git a/core/type-checker/src/type_checker.rs b/core/type-checker/src/type_checker.rs index 28a5f8e1..0b751db0 100644 --- a/core/type-checker/src/type_checker.rs +++ b/core/type-checker/src/type_checker.rs @@ -651,19 +651,10 @@ impl TypeChecker { Statement::Loop(loop_statement) => { if let Some(condition) = &*loop_statement.condition.borrow() { let cond_type = self.infer_expression(condition, ctx); - if !cond_type.is_error() { - if !cond_type.is_bool() { - self.errors.push(TypeCheckError::TypeMismatch { - expected: TypeInfo::boolean(), - found: cond_type, - context: TypeMismatchContext::Condition, - location: loop_statement.location, - }); - } - } else { + if !cond_type.is_error() && !cond_type.is_bool() { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), - found: TypeInfo::default(), + found: cond_type, context: TypeMismatchContext::Condition, location: loop_statement.location, }); @@ -678,19 +669,10 @@ impl TypeChecker { Statement::Break(_) => {} Statement::If(if_statement) => { let cond_type = self.infer_expression(&if_statement.condition.borrow(), ctx); - if !cond_type.is_error() { - if !cond_type.is_bool() { - self.errors.push(TypeCheckError::TypeMismatch { - expected: TypeInfo::boolean(), - found: cond_type, - context: TypeMismatchContext::Condition, - location: if_statement.location, - }); - } - } else { + if !cond_type.is_error() && !cond_type.is_bool() { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), - found: TypeInfo::default(), + found: cond_type, context: TypeMismatchContext::Condition, location: if_statement.location, }); @@ -761,19 +743,10 @@ impl TypeChecker { Statement::Assert(assert_statement) => { let cond_type = self.infer_expression(&assert_statement.expression.borrow(), ctx); - if !cond_type.is_error() { - if !cond_type.is_bool() { - self.errors.push(TypeCheckError::TypeMismatch { - expected: TypeInfo::boolean(), - found: cond_type, - context: TypeMismatchContext::Condition, - location: assert_statement.location, - }); - } - } else { + if !cond_type.is_error() && !cond_type.is_bool() { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), - found: TypeInfo::default(), + found: cond_type, context: TypeMismatchContext::Condition, location: assert_statement.location, }); @@ -1219,6 +1192,11 @@ impl TypeChecker { name: function_call_expression.name(), location: function_call_expression.location, }); + if let Some(arguments) = &function_call_expression.arguments { + for arg in arguments { + self.infer_expression(&arg.1.borrow(), ctx); + } + } return TypeInfo::error("Poisoned type from error"); }; if let Some(arguments) = &function_call_expression.arguments @@ -1401,17 +1379,25 @@ impl TypeChecker { } let res_type = match binary_expression.operator { OperatorKind::And | OperatorKind::Or => { - if left_type.is_bool() && right_type.is_bool() { - TypeInfo::boolean() + if !left_type.is_error() && !right_type.is_error() { + if left_type.is_bool() && right_type.is_bool() { + TypeInfo::boolean() + } else { + self.errors.push(TypeCheckError::InvalidBinaryOperand { + operator: binary_expression.operator.clone(), + expected_kind: "logical", + operand_desc: "non-boolean types", + found_types: (left_type, right_type), + location: binary_expression.location, + }); + TypeInfo::error("Invalid binary operand") + } } else { - self.errors.push(TypeCheckError::InvalidBinaryOperand { - operator: binary_expression.operator.clone(), - expected_kind: "logical", - operand_desc: "non-boolean types", - found_types: (left_type, right_type), - location: binary_expression.location, - }); - TypeInfo::error("Invalid binary operand") + if left_type.is_error() { + left_type + } else { + right_type + } } } OperatorKind::Eq @@ -1432,7 +1418,7 @@ impl TypeChecker { | OperatorKind::BitNot | OperatorKind::Shl | OperatorKind::Shr => { - if !left_type.is_number() || !right_type.is_number() { + if !left_type.is_error() && !right_type.is_error() && (!left_type.is_number() || !right_type.is_number()) { self.errors.push(TypeCheckError::InvalidBinaryOperand { operator: binary_expression.operator.clone(), expected_kind: "arithmetic", @@ -1441,14 +1427,7 @@ impl TypeChecker { location: binary_expression.location, }); } - if left_type != right_type { - self.errors.push(TypeCheckError::BinaryOperandTypeMismatch { - operator: binary_expression.operator.clone(), - left: left_type.clone(), - right: right_type, - location: binary_expression.location, - }); - } + // Removed duplicate BinaryOperandTypeMismatch check here left_type.clone() } }; @@ -1461,31 +1440,38 @@ impl TypeChecker { return type_info; } if let Some(elements) = &array_literal.elements { - let element_type_info = self.infer_expression(&elements[0].borrow(), ctx); - for element in &elements[1..] { - let element_type = self.infer_expression(&element.borrow(), ctx); - 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, - }); + if let Some(first) = elements.first() { + let element_type_info = self.infer_expression(&first.borrow(), ctx); + for element in &elements[1..] { + let element_type = self.infer_expression(&element.borrow(), ctx); + 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 { + kind: TypeInfoKind::Array( + Box::new(element_type_info), + elements.len() as u32, + ), + type_params: vec![], + }; + ctx.set_node_typeinfo(array_literal.id, array_type.clone()); + return array_type; } - let array_type = TypeInfo { - kind: TypeInfoKind::Array( - Box::new(element_type_info), - elements.len() as u32, - ), - type_params: vec![], - }; - ctx.set_node_typeinfo(array_literal.id, array_type.clone()); - return array_type; } - TypeInfo::error("Empty array literal") + let array_type = TypeInfo { + kind: TypeInfoKind::Array(Box::new(TypeInfo::default()), 0), + type_params: vec![], + }; + ctx.set_node_typeinfo(array_literal.id, array_type.clone()); + array_type } Literal::Bool(_) => { ctx.set_node_typeinfo(literal.id(), TypeInfo::boolean()); diff --git a/tests/src/type_checker/error_type_poisoning_tests.rs b/tests/src/type_checker/error_type_poisoning_tests.rs index 1c3dcdf9..6916450b 100644 --- a/tests/src/type_checker/error_type_poisoning_tests.rs +++ b/tests/src/type_checker/error_type_poisoning_tests.rs @@ -8,7 +8,7 @@ //! //! 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}; From 57bd8ad26c7e5f2b162d9c09a5e86561808a470d Mon Sep 17 00:00:00 2001 From: Chibuikem Michael Ilonze Date: Fri, 30 Jan 2026 14:02:54 +0100 Subject: [PATCH 6/6] fix: address round 2 review feedback for error type poisoning Critical fixes: - Arithmetic ops now propagate Error symmetrically (5+unknown == unknown+5) - Method call on error receiver now infers arguments before returning - Changed Error(String) to unit variant Error for proper equality semantics Significant fixes: - Comparison operators now propagate Error for consistency - Uzumaki expression now emits diagnostic when type not inferred - Added comments explaining defensive guards on declared types - Tests now verify error counts using split('; ') Style fixes: - Fixed double space in Statement::If - Added #[must_use] reason strings to error() and is_error() - Added #[cfg(test)] attribute to test module - Added doc comments to error() and is_error() methods --- core/type-checker/src/errors.rs | 8 +- core/type-checker/src/type_checker.rs | 103 +++++++++++++----- core/type-checker/src/type_info.rs | 33 ++++-- .../error_type_poisoning_tests.rs | 72 ++++++------ 4 files changed, 141 insertions(+), 75 deletions(-) diff --git a/core/type-checker/src/errors.rs b/core/type-checker/src/errors.rs index 0703d8f2..015eae79 100644 --- a/core/type-checker/src/errors.rs +++ b/core/type-checker/src/errors.rs @@ -399,7 +399,9 @@ pub enum TypeCheckError { /// /// This occurs when `Type::method()` syntax is used for a method that requires `self`. /// Use `instance.method()` instead. - #[error("{location}: instance method `{type_name}::{method_name}` requires a receiver, use `instance.{method_name}()` instead")] + #[error( + "{location}: instance method `{type_name}::{method_name}` requires a receiver, use `instance.{method_name}()` instead" + )] InstanceMethodCalledAsAssociated { type_name: String, method_name: String, @@ -410,7 +412,9 @@ pub enum TypeCheckError { /// /// This occurs when `instance.function()` syntax is used for an associated function /// that doesn't take `self`. Use `Type::function()` instead. - #[error("{location}: associated function `{type_name}::{method_name}` cannot be called on an instance, use `{type_name}::{method_name}()` instead")] + #[error( + "{location}: associated function `{type_name}::{method_name}` cannot be called on an instance, use `{type_name}::{method_name}()` instead" + )] AssociatedFunctionCalledAsMethod { type_name: String, method_name: String, diff --git a/core/type-checker/src/type_checker.rs b/core/type-checker/src/type_checker.rs index 0b751db0..e26f6cfa 100644 --- a/core/type-checker/src/type_checker.rs +++ b/core/type-checker/src/type_checker.rs @@ -606,7 +606,10 @@ impl TypeChecker { } } else { let value_type = self.infer_expression(&right_expr, ctx); - if !target_type.is_error() && !value_type.is_error() && target_type != value_type { + if !target_type.is_error() + && !value_type.is_error() + && target_type != value_type + { self.errors.push(TypeCheckError::TypeMismatch { expected: target_type, found: value_type, @@ -636,8 +639,10 @@ impl TypeChecker { return_type.clone(), ); } else { - let val = - self.infer_expression(&return_statement.expression.borrow(), ctx); + let val = self.infer_expression(&return_statement.expression.borrow(), ctx); + // Note: return_type.is_error() is a defensive guard; declared return types + // from TypeInfo::new() never produce Error, but this protects against + // future changes. if !return_type.is_error() && !val.is_error() && *return_type != val { self.errors.push(TypeCheckError::TypeMismatch { expected: return_type.clone(), @@ -669,7 +674,7 @@ impl TypeChecker { Statement::Break(_) => {} Statement::If(if_statement) => { let cond_type = self.infer_expression(&if_statement.condition.borrow(), ctx); - if !cond_type.is_error() && !cond_type.is_bool() { + if !cond_type.is_error() && !cond_type.is_bool() { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), found: cond_type, @@ -699,6 +704,9 @@ impl TypeChecker { ctx.set_node_typeinfo(uzumaki_rc.id, target_type.clone()); } else { let init_type = self.infer_expression(&expr_ref, ctx); + // Note: target_type.is_error() is a defensive guard; declared types + // from TypeInfo::new() never produce Error, but this protects against + // future changes. if !target_type.is_error() && !init_type.is_error() && init_type != TypeInfo::new(&variable_definition_statement.ty) @@ -741,8 +749,7 @@ impl TypeChecker { } } Statement::Assert(assert_statement) => { - let cond_type = - self.infer_expression(&assert_statement.expression.borrow(), ctx); + let cond_type = self.infer_expression(&assert_statement.expression.borrow(), ctx); if !cond_type.is_error() && !cond_type.is_bool() { self.errors.push(TypeCheckError::TypeMismatch { expected: TypeInfo::boolean(), @@ -772,18 +779,16 @@ impl TypeChecker { } #[allow(clippy::too_many_lines)] - fn infer_expression( - &mut self, - expression: &Expression, - ctx: &mut TypedContext, - ) -> TypeInfo { + fn infer_expression(&mut self, expression: &Expression, ctx: &mut TypedContext) -> TypeInfo { match expression { Expression::ArrayIndexAccess(array_index_access_expression) => { if let Some(type_info) = ctx.get_node_typeinfo(array_index_access_expression.id) { type_info.clone() } else { - let array_type = self.infer_expression(&array_index_access_expression.array.borrow(), ctx); - let index_type = self.infer_expression(&array_index_access_expression.index.borrow(), ctx); + let array_type = + self.infer_expression(&array_index_access_expression.array.borrow(), ctx); + let index_type = + self.infer_expression(&array_index_access_expression.index.borrow(), ctx); if !index_type.is_error() && !index_type.is_number() { self.errors.push(TypeCheckError::ArrayIndexNotNumeric { @@ -801,7 +806,7 @@ impl TypeChecker { ); element_type } - TypeInfoKind::Error(_) => array_type.clone(), + TypeInfoKind::Error => array_type.clone(), _ => { self.errors.push(TypeCheckError::ExpectedArrayType { found: array_type, @@ -816,7 +821,8 @@ impl TypeChecker { if let Some(type_info) = ctx.get_node_typeinfo(member_access_expression.id) { type_info.clone() } else { - let object_type = self.infer_expression(&member_access_expression.expression.borrow(), ctx); + let 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 object_type; @@ -899,7 +905,9 @@ impl TypeChecker { found: TypeInfo::new(ty), location: type_member_access_expression.location, }); - return TypeInfo::error("Expected enum type for variant access"); + return TypeInfo::error( + "Expected enum type for variant access", + ); } } } @@ -914,7 +922,7 @@ impl TypeChecker { match &expr_type.kind { TypeInfoKind::Enum(name) => name.clone(), - TypeInfoKind::Error(_) => return expr_type, + TypeInfoKind::Error => return expr_type, _ => { self.errors.push(TypeCheckError::ExpectedEnumType { found: expr_type, @@ -1070,9 +1078,16 @@ impl TypeChecker { if let Expression::MemberAccess(member_access) = &function_call_expression.function { - let receiver_type = self.infer_expression(&member_access.expression.borrow(), ctx); + let receiver_type = + self.infer_expression(&member_access.expression.borrow(), ctx); if receiver_type.is_error() { + // Infer arguments even when receiver is error for better error recovery + if let Some(arguments) = &function_call_expression.arguments { + for arg in arguments { + self.infer_expression(&arg.1.borrow(), ctx); + } + } return receiver_type; } let type_name = match &receiver_type.kind { @@ -1405,7 +1420,16 @@ impl TypeChecker { | OperatorKind::Lt | OperatorKind::Le | OperatorKind::Gt - | OperatorKind::Ge => TypeInfo::boolean(), + | OperatorKind::Ge => { + // Propagate error types for consistency with other operators + if left_type.is_error() { + left_type.clone() + } else if right_type.is_error() { + right_type.clone() + } else { + TypeInfo::boolean() + } + } OperatorKind::Pow | OperatorKind::Add | OperatorKind::Sub @@ -1418,17 +1442,24 @@ impl TypeChecker { | OperatorKind::BitNot | OperatorKind::Shl | OperatorKind::Shr => { - if !left_type.is_error() && !right_type.is_error() && (!left_type.is_number() || !right_type.is_number()) { - self.errors.push(TypeCheckError::InvalidBinaryOperand { - operator: binary_expression.operator.clone(), - expected_kind: "arithmetic", - operand_desc: "non-number types", - found_types: (left_type.clone(), right_type.clone()), - location: binary_expression.location, - }); + // Propagate error types symmetrically: 5 + unknown and unknown + 5 + // should both return Error type + if left_type.is_error() { + left_type.clone() + } else if right_type.is_error() { + right_type.clone() + } else { + if !left_type.is_number() || !right_type.is_number() { + self.errors.push(TypeCheckError::InvalidBinaryOperand { + operator: binary_expression.operator.clone(), + expected_kind: "arithmetic", + operand_desc: "non-number types", + found_types: (left_type.clone(), right_type.clone()), + location: binary_expression.location, + }); + } + left_type.clone() } - // Removed duplicate BinaryOperandTypeMismatch check here - left_type.clone() } }; ctx.set_node_typeinfo(binary_expression.id, res_type.clone()); @@ -1519,7 +1550,19 @@ impl TypeChecker { } type_info } - Expression::Uzumaki(uzumaki) => ctx.get_node_typeinfo(uzumaki.id).unwrap_or_else(|| TypeInfo::error("Uzumaki type not inferred")), + Expression::Uzumaki(uzumaki) => { + if let Some(ti) = ctx.get_node_typeinfo(uzumaki.id) { + ti + } else { + // This path should be unreachable: Uzumaki types are always set + // by the containing statement (VariableDefinition, Assign, Return). + // If we reach here, it indicates a bug in the type checker. + self.errors.push(TypeCheckError::CannotInferUzumakiType { + location: uzumaki.location, + }); + TypeInfo::error("Uzumaki type not inferred") + } + } } } diff --git a/core/type-checker/src/type_info.rs b/core/type-checker/src/type_info.rs index 1f5ea057..e9a4119c 100644 --- a/core/type-checker/src/type_info.rs +++ b/core/type-checker/src/type_info.rs @@ -194,7 +194,12 @@ pub enum TypeInfoKind { Struct(String), Enum(String), Spec(String), - Error(String), + /// Represents a type error during type checking. + /// + /// All Error types are equal to each other, following rustc's TyKind::Error + /// pattern. This ensures that comparisons between error types don't produce + /// spurious type mismatch diagnostics. + Error, } impl Display for TypeInfoKind { @@ -213,7 +218,7 @@ impl Display for TypeInfoKind { | TypeInfoKind::Qualified(ty) | TypeInfoKind::Function(ty) => write!(f, "{ty}"), TypeInfoKind::Generic(ty) => write!(f, "{ty}'"), - TypeInfoKind::Error(msg) => write!(f, "{{error: {msg}}}"), + TypeInfoKind::Error => write!(f, "{{error}}"), } } } @@ -309,10 +314,15 @@ impl TypeInfo { } } - #[must_use] - pub fn error(msg: impl Into) -> Self { + /// Creates an error type to represent a failed type lookup or inference. + /// + /// Error types are used to poison the type graph when an error occurs, + /// preventing cascading errors from being reported. All error types are + /// equal to each other. + #[must_use = "this returns a new TypeInfo, it does not modify anything"] + pub fn error(_msg: impl Into) -> Self { Self { - kind: TypeInfoKind::Error(msg.into()), + kind: TypeInfoKind::Error, type_params: vec![], } } @@ -436,9 +446,14 @@ impl TypeInfo { matches!(self.kind, TypeInfoKind::Generic(_)) } - #[must_use] + /// Returns true if this is an error type. + /// + /// Error types are used to suppress cascading errors in the type checker. + /// When an expression has an error type, downstream operations should + /// propagate the error without reporting additional diagnostics. + #[must_use = "this is a pure check with no side effects"] pub fn is_error(&self) -> bool { - matches!(self.kind, TypeInfoKind::Error(_)) + matches!(self.kind, TypeInfoKind::Error) } /// Returns true if this is a signed integer type (i8, i16, i32, i64). @@ -485,7 +500,7 @@ impl TypeInfo { | TypeInfoKind::Struct(_) | TypeInfoKind::Enum(_) | TypeInfoKind::Spec(_) - | TypeInfoKind::Error(_) => self.clone(), + | TypeInfoKind::Error => self.clone(), } } @@ -507,7 +522,7 @@ impl TypeInfo { | TypeInfoKind::Struct(_) | TypeInfoKind::Enum(_) | TypeInfoKind::Spec(_) - | TypeInfoKind::Error(_) => false, + | TypeInfoKind::Error => false, } } diff --git a/tests/src/type_checker/error_type_poisoning_tests.rs b/tests/src/type_checker/error_type_poisoning_tests.rs index 6916450b..08dfd193 100644 --- a/tests/src/type_checker/error_type_poisoning_tests.rs +++ b/tests/src/type_checker/error_type_poisoning_tests.rs @@ -8,7 +8,7 @@ //! //! 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}; @@ -53,7 +53,8 @@ mod error_type_poisoning_tests { #[test] fn test_error_type_display() { let error_type = TypeInfo::error("test message"); - assert_eq!(error_type.to_string(), "{error: test message}"); + // Error types now display without message (unit variant) + assert_eq!(error_type.to_string(), "{error}"); } #[test] @@ -87,19 +88,18 @@ mod error_type_poisoning_tests { if let Err(error) = result { let error_msg = error.to_string(); + let errors: Vec<&str> = error_msg.split("; ").collect(); + assert_eq!( + errors.len(), + 1, + "Should produce exactly 1 error (undeclared variable), got: {:?}", + errors + ); assert!( error_msg.contains("unknown_var"), "Should report undeclared variable: {}", error_msg ); - // Should NOT have type mismatch between i32 and {error} - let type_mismatch_unknown = - error_msg.contains("type mismatch") && error_msg.contains("{error}"); - assert!( - !type_mismatch_unknown, - "Should NOT report type mismatch with {{error}}: {}", - error_msg - ); } } @@ -131,19 +131,18 @@ mod error_type_poisoning_tests { if let Err(error) = result { let error_msg = error.to_string(); + let errors: Vec<&str> = error_msg.split("; ").collect(); + assert_eq!( + errors.len(), + 1, + "Should produce exactly 1 error (undefined function), got: {:?}", + errors + ); assert!( error_msg.contains("undefined_func"), "Should report undefined function: {}", error_msg ); - // Should NOT have type mismatch between i32 and {error} - let type_mismatch_unknown = - error_msg.contains("type mismatch") && error_msg.contains("{error}"); - assert!( - !type_mismatch_unknown, - "Should NOT report type mismatch with {{error}}: {}", - error_msg - ); } } @@ -156,13 +155,18 @@ mod error_type_poisoning_tests { if let Err(error) = result { let error_msg = error.to_string(); + let errors: Vec<&str> = error_msg.split("; ").collect(); + assert_eq!( + errors.len(), + 1, + "Should produce exactly 1 error (undeclared variable), got: {:?}", + errors + ); 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 } } @@ -211,20 +215,18 @@ mod error_type_poisoning_tests { if let Err(error) = result { let error_msg = error.to_string(); + let errors: Vec<&str> = error_msg.split("; ").collect(); + assert_eq!( + errors.len(), + 1, + "Should produce exactly 1 error (undeclared variable), got: {:?}", + errors + ); 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("{error}"); - assert!( - !type_mismatch_unknown, - "Should NOT report type mismatch with {{error}}: {}", - error_msg - ); } } @@ -434,12 +436,14 @@ mod error_type_poisoning_tests { #[test] fn test_error_types_are_equal() { - let error1 = TypeInfo::error("same message"); - let error2 = TypeInfo::error("same message"); - assert_eq!(error1, error2); - + // All error types are equal (unit variant without payload) + let error1 = TypeInfo::error("message one"); + let error2 = TypeInfo::error("message two"); let error3 = TypeInfo::error("different message"); - assert_ne!(error1, error3); + // All Error types should be equal regardless of the message passed in + assert_eq!(error1, error2); + assert_eq!(error1, error3); + assert_eq!(error2, error3); } #[test]