From 5a07b82ee419d46b4350dad912977f2da282cc3f Mon Sep 17 00:00:00 2001 From: od-hunter Date: Thu, 29 Jan 2026 00:51:49 +0100 Subject: [PATCH] Prohibit double unary operators (-- and ~~) --- core/type-checker/src/errors.rs | 39 +++++++++++++++- core/type-checker/src/type_checker.rs | 15 ++++++ tests/src/type_checker/coverage.rs | 66 +++++++++++++++++++++++++-- 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/core/type-checker/src/errors.rs b/core/type-checker/src/errors.rs index 0703d8f2..d0323fb1 100644 --- a/core/type-checker/src/errors.rs +++ b/core/type-checker/src/errors.rs @@ -416,6 +416,16 @@ pub enum TypeCheckError { method_name: String, location: Location, }, + + /// Double unary operator is prohibited. + /// + /// This occurs when the same unary operator is applied twice consecutively, + /// such as `--x` or `~~x`. These patterns are prohibited as they are likely mistakes. + #[error("{location}: double unary operator `{operator:?}{operator:?}` is prohibited")] + DoubleUnaryOperator { + operator: UnaryOperatorKind, + location: Location, + }, } impl TypeCheckError { @@ -455,7 +465,8 @@ impl TypeCheckError { | TypeCheckError::ConflictingTypeInference { location, .. } | TypeCheckError::PrivateAccessViolation { location, .. } | TypeCheckError::InstanceMethodCalledAsAssociated { location, .. } - | TypeCheckError::AssociatedFunctionCalledAsMethod { location, .. } => location, + | TypeCheckError::AssociatedFunctionCalledAsMethod { location, .. } + | TypeCheckError::DoubleUnaryOperator { location, .. } => location, } } } @@ -1030,4 +1041,28 @@ mod tests { assert!(msg.contains("new")); assert!(msg.contains("cannot be called on an instance")); } -} + + #[test] + fn display_double_unary_operator() { + let err = TypeCheckError::DoubleUnaryOperator { + operator: UnaryOperatorKind::Neg, + location: test_location(), + }; + let msg = err.to_string(); + assert!(msg.contains("double unary operator")); + assert!(msg.contains("Neg")); + assert!(msg.contains("prohibited")); + } + + #[test] + fn display_double_unary_operator_bitnot() { + let err = TypeCheckError::DoubleUnaryOperator { + operator: UnaryOperatorKind::BitNot, + location: test_location(), + }; + let msg = err.to_string(); + assert!(msg.contains("double unary operator")); + assert!(msg.contains("BitNot")); + assert!(msg.contains("prohibited")); + } +} \ No newline at end of file diff --git a/core/type-checker/src/type_checker.rs b/core/type-checker/src/type_checker.rs index 82de1b2d..fc3a08da 100644 --- a/core/type-checker/src/type_checker.rs +++ b/core/type-checker/src/type_checker.rs @@ -1309,6 +1309,21 @@ impl TypeChecker { None } Expression::PrefixUnary(prefix_unary_expression) => { + // Check for prohibited double unary operators (-- or ~~) + if let Expression::PrefixUnary(inner_unary) = + &*prefix_unary_expression.expression.borrow() + && prefix_unary_expression.operator == inner_unary.operator + && matches!( + prefix_unary_expression.operator, + UnaryOperatorKind::Neg | UnaryOperatorKind::BitNot + ) + { + self.errors.push(TypeCheckError::DoubleUnaryOperator { + operator: prefix_unary_expression.operator.clone(), + location: prefix_unary_expression.location, + }); + return None; + } match prefix_unary_expression.operator { UnaryOperatorKind::Not => { let expression_type_op = self diff --git a/tests/src/type_checker/coverage.rs b/tests/src/type_checker/coverage.rs index a1c74344..32caff39 100644 --- a/tests/src/type_checker/coverage.rs +++ b/tests/src/type_checker/coverage.rs @@ -748,12 +748,72 @@ mod expression_coverage { } #[test] - fn test_unary_neg_nested() { - let source = r#"fn test() -> i32 { return --42; }"#; + fn test_double_unary_neg_prohibited() { + let source = r#"fn test(x: i32) -> i32 { return --x; }"#; + let result = try_type_check(source); + assert!( + result.is_err(), + "Double unary neg (--) should be prohibited" + ); + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("double unary operator"), + "Error should mention double unary operator: {}", + error_msg + ); + } + } + + #[test] + fn test_double_unary_bitnot_prohibited() { + let source = r#"fn test(x: i32) -> i32 { return ~~x; }"#; + let result = try_type_check(source); + assert!( + result.is_err(), + "Double unary bitnot (~~) should be prohibited" + ); + if let Err(error) = result { + let error_msg = error.to_string(); + assert!( + error_msg.contains("double unary operator"), + "Error should mention double unary operator: {}", + error_msg + ); + } + } + + #[test] + fn test_mixed_unary_operators_allowed() { + // -~x and ~-x should still be allowed (different operators) + let source = r#"fn test(x: i32) -> i32 { return -~x; }"#; + let result = try_type_check(source); + assert!( + result.is_ok(), + "Mixed unary operators (-~) should work, got: {:?}", + result.err() + ); + } + + #[test] + fn test_mixed_unary_operators_reversed_allowed() { + let source = r#"fn test(x: i32) -> i32 { return ~-x; }"#; + let result = try_type_check(source); + assert!( + result.is_ok(), + "Mixed unary operators (~-) should work, got: {:?}", + result.err() + ); + } + + #[test] + fn test_double_not_allowed() { + // !!x is logical NOT twice, which is allowed (returns boolean) + let source = r#"fn test(b: bool) -> bool { return !!b; }"#; let result = try_type_check(source); assert!( result.is_ok(), - "Double unary neg should work, got: {:?}", + "Double logical NOT (!!) should be allowed, got: {:?}", result.err() ); }