Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions core/type-checker/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
}
}
}
Expand Down Expand Up @@ -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"));
}
}
15 changes: 15 additions & 0 deletions core/type-checker/src/type_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 63 additions & 3 deletions tests/src/type_checker/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
}
Expand Down