use std::collections::HashSet;
use crate::{
declaration::{Declaration, Module},
expression::{Block, Expression, ExpressionKind, Span, Statement},
operator::{ArithmeticOperator, BinaryOperator},
types::Type,
};
fn spanned(span: Span, message: impl std::fmt::Display) -> String {
if span.line > 0 {
format!("{span}: {message}")
} else {
message.to_string()
}
}
pub fn lint_module(module: &Module) -> Vec<String> {
let mut errors = Vec::new();
for declaration in module {
if let Declaration::Function(function) = declaration {
let pointer_params: HashSet<String> = function
.parameters
.iter()
.filter(|p| matches!(&p.parameter_type, Some(Type::Pointer(..))))
.map(|p| p.name.clone())
.collect();
lint_block(&function.body, &pointer_params, &mut errors);
lint_unused_bindings(&function.body, &mut errors);
lint_dead_labels(&function.body, &mut errors);
lint_recursion(&function.name, &function.body, &mut errors);
lint_nesting_depth(&function.body, 0, 4, &mut errors);
lint_unnecessary_return(&function.body, &mut errors);
lint_unnecessary_intermediate(&function.body, &mut errors);
}
}
errors
}
fn lint_block(block: &Block, pointer_params: &HashSet<String>, errors: &mut Vec<String>) {
let mut found_return = false;
for statement in &block.statements {
if found_return {
errors.push("unreachable code after return".into());
break;
}
lint_statement(statement, pointer_params, errors);
if matches!(statement, Statement::Return(_)) {
found_return = true;
}
}
if let Some(result) = &block.result {
lint_expression(result, pointer_params, errors);
}
}
fn lint_statement(
statement: &Statement,
pointer_params: &HashSet<String>,
errors: &mut Vec<String>,
) {
match statement {
Statement::Expression(expression) => {
if let ExpressionKind::Replace(target, value) = &expression.kind {
if same_variable(target, value)
&& let Some(name) = variable_name(target)
{
errors.push(spanned(
expression.span,
format!("self-assignment: '{name} := {name}' has no effect"),
));
}
check_compound_assignment(expression.span, target, value, errors);
check_chained_swap(target, value, pointer_params, errors);
}
lint_expression(expression, pointer_params, errors);
}
Statement::Let { value, .. } => lint_expression(value, pointer_params, errors),
Statement::Assign(target, value) => {
lint_expression(target, pointer_params, errors);
lint_expression(value, pointer_params, errors);
}
Statement::Return(Some(expression)) => lint_expression(expression, pointer_params, errors),
Statement::Label {
initial_arguments, ..
} => {
for argument in initial_arguments {
lint_expression(argument, pointer_params, errors);
}
}
Statement::Jump { arguments, .. } => {
for argument in arguments {
lint_expression(argument, pointer_params, errors);
}
}
Statement::MultiReplace { values, .. } => {
for value in values {
lint_expression(value, pointer_params, errors);
}
}
Statement::Defer(inner) => lint_statement(inner, pointer_params, errors),
Statement::Return(None) => {}
}
}
fn lint_expression(
expression: &Expression,
pointer_params: &HashSet<String>,
errors: &mut Vec<String>,
) {
match &expression.kind {
ExpressionKind::BinaryOperation(operator, left, right) => {
if matches!(
operator,
crate::operator::BinaryOperator::Arithmetic(
ArithmeticOperator::Divide | ArithmeticOperator::Remainder
)
) && matches!(
right.kind,
ExpressionKind::Literal(crate::expression::Literal::Integer(0))
) {
errors.push(spanned(expression.span, "division by zero literal"));
}
if matches!(
operator,
crate::operator::BinaryOperator::Comparison(
crate::operator::ComparisonOperator::Equal
| crate::operator::ComparisonOperator::NotEqual
)
) {
let is_bool_literal = |expression: &Expression| {
matches!(
expression.kind,
ExpressionKind::Literal(crate::expression::Literal::Bool(_))
)
};
if is_bool_literal(left) || is_bool_literal(right) {
errors.push(
"comparison with bool literal: use 'x' instead of 'x == true', '!x' instead of 'x == false'".into(),
);
}
}
lint_expression(left, pointer_params, errors);
lint_expression(right, pointer_params, errors);
}
ExpressionKind::UnaryOperation(operator, operand) => {
if let ExpressionKind::UnaryOperation(inner_operator, _) = &operand.kind
&& operator == inner_operator
{
errors.push("double negation can be simplified".into());
}
lint_expression(operand, pointer_params, errors);
}
ExpressionKind::Dereference(operand)
| ExpressionKind::Convert(operand, _)
| ExpressionKind::Transmute(operand, _) => {
lint_expression(operand, pointer_params, errors);
}
ExpressionKind::Call(callee, arguments) => {
lint_expression(callee, pointer_params, errors);
for argument in arguments {
lint_expression(argument, pointer_params, errors);
}
}
ExpressionKind::Replace(target, value) => {
lint_expression(target, pointer_params, errors);
lint_expression(value, pointer_params, errors);
}
ExpressionKind::If {
condition,
then_branch,
else_branch,
} => {
lint_expression(condition, pointer_params, errors);
lint_block(then_branch, pointer_params, errors);
if let Some(else_block) = else_branch {
lint_block(else_block, pointer_params, errors);
}
}
ExpressionKind::Match { value, arms } => {
lint_expression(value, pointer_params, errors);
for arm in arms {
lint_block(&arm.body, pointer_params, errors);
}
}
ExpressionKind::Block(block) => lint_block(block, pointer_params, errors),
ExpressionKind::Field(object, _) => lint_expression(object, pointer_params, errors),
ExpressionKind::Index(object, index) => {
lint_expression(object, pointer_params, errors);
lint_expression(index, pointer_params, errors);
}
ExpressionKind::ArrayLiteral(elements) => {
for element in elements {
lint_expression(element, pointer_params, errors);
}
}
ExpressionKind::Print(arguments) => {
for argument in arguments {
lint_expression(argument, pointer_params, errors);
}
}
ExpressionKind::TypeConstruction(_, fields) => {
for (_, value) in fields {
lint_expression(value, pointer_params, errors);
}
}
ExpressionKind::Slice(array, start, end) => {
lint_expression(array, pointer_params, errors);
if let Some(start) = start {
lint_expression(start, pointer_params, errors);
}
if let Some(end) = end {
lint_expression(end, pointer_params, errors);
}
}
_ => {}
}
}
fn variable_name(expression: &Expression) -> Option<&str> {
match &expression.kind {
ExpressionKind::Variable(name) => Some(name),
ExpressionKind::Dereference(inner) => variable_name(inner),
_ => None,
}
}
fn same_variable(a: &Expression, b: &Expression) -> bool {
match (variable_name(a), variable_name(b)) {
(Some(name_a), Some(name_b)) => name_a == name_b,
_ => false,
}
}
fn check_compound_assignment(
span: Span,
target: &Expression,
value: &Expression,
errors: &mut Vec<String>,
) {
let Some(target_name) = variable_name(target) else {
return;
};
let ExpressionKind::BinaryOperation(operator, left, _) = &value.kind else {
return;
};
let operator_symbol = match operator {
BinaryOperator::Arithmetic(ArithmeticOperator::Add) => "+",
BinaryOperator::Arithmetic(ArithmeticOperator::Subtract) => "-",
BinaryOperator::Arithmetic(ArithmeticOperator::Multiply) => "*",
BinaryOperator::Arithmetic(ArithmeticOperator::Divide) => "/",
_ => return,
};
if variable_name(left).is_some_and(|n| n == target_name) {
errors.push(spanned(
span,
format!(
"use '{target_name} {operator_symbol}= <value>' instead of '{target_name} := {target_name} {operator_symbol} <value>'"
),
));
}
}
fn check_chained_swap(
target: &Expression,
value: &Expression,
_pointer_params: &HashSet<String>,
errors: &mut Vec<String>,
) {
let ExpressionKind::Replace(inner_target, inner_value) = &value.kind else {
return;
};
if !same_variable(inner_value, target) {
return;
}
let Some(name_a) = variable_name(target) else {
return;
};
let Some(name_b) = variable_name(inner_target) else {
return;
};
errors.push(format!(
"use '{name_a}, {name_b} := {name_b}, {name_a}' instead of '{name_a} := {name_b} := {name_a}'"
));
}
fn lint_unused_bindings(block: &Block, errors: &mut Vec<String>) {
let mut defined: Vec<String> = Vec::new();
let mut used: HashSet<String> = HashSet::new();
collect_definitions_block(block, &mut defined);
collect_usages_block(block, &mut used);
for name in &defined {
if !name.starts_with('_') && !used.contains(name) {
errors.push(format!(
"unused binding '{name}' (prefix with _ to suppress)"
));
}
}
}
fn collect_definitions_block(block: &Block, definitions: &mut Vec<String>) {
for statement in &block.statements {
collect_definitions_statement(statement, definitions);
}
}
fn collect_definitions_statement(statement: &Statement, definitions: &mut Vec<String>) {
match statement {
Statement::Let { name, .. } => definitions.push(name.clone()),
Statement::Label { parameters, .. } => {
for parameter in parameters {
definitions.push(parameter.name.clone());
}
}
Statement::Expression(expression) => {
collect_definitions_expression(expression, definitions)
}
Statement::Defer(inner) => collect_definitions_statement(inner, definitions),
Statement::MultiReplace { bindings, .. } => {
for (name, _) in bindings.iter().flatten() {
definitions.push(name.clone());
}
}
_ => {}
}
}
fn collect_definitions_expression(expression: &Expression, definitions: &mut Vec<String>) {
match &expression.kind {
ExpressionKind::If {
then_branch,
else_branch,
..
} => {
collect_definitions_block(then_branch, definitions);
if let Some(else_block) = else_branch {
collect_definitions_block(else_block, definitions);
}
}
ExpressionKind::Block(block) => collect_definitions_block(block, definitions),
_ => {}
}
}
fn collect_usages_block(block: &Block, used: &mut HashSet<String>) {
for statement in &block.statements {
collect_usages_statement(statement, used);
}
if let Some(result) = &block.result {
collect_usages_expression(result, used);
}
}
fn collect_usages_statement(statement: &Statement, used: &mut HashSet<String>) {
match statement {
Statement::Expression(expression) => collect_usages_expression(expression, used),
Statement::Let { value, .. } => collect_usages_expression(value, used),
Statement::Assign(target, value) => {
collect_usages_expression(target, used);
collect_usages_expression(value, used);
}
Statement::Return(Some(expression)) => collect_usages_expression(expression, used),
Statement::Label {
initial_arguments, ..
} => {
for argument in initial_arguments {
collect_usages_expression(argument, used);
}
}
Statement::Jump { arguments, .. } => {
for argument in arguments {
collect_usages_expression(argument, used);
}
}
Statement::MultiReplace {
targets, values, ..
} => {
for target in targets {
collect_usages_expression(target, used);
}
for value in values {
collect_usages_expression(value, used);
}
}
Statement::Defer(inner) => collect_usages_statement(inner, used),
Statement::Return(None) => {}
}
}
fn collect_usages_expression(expression: &Expression, used: &mut HashSet<String>) {
match &expression.kind {
ExpressionKind::Variable(name) => {
used.insert(name.clone());
}
ExpressionKind::BinaryOperation(_, left, right) | ExpressionKind::Replace(left, right) => {
collect_usages_expression(left, used);
collect_usages_expression(right, used);
}
ExpressionKind::UnaryOperation(_, operand)
| ExpressionKind::Dereference(operand)
| ExpressionKind::Convert(operand, _)
| ExpressionKind::Transmute(operand, _) => {
collect_usages_expression(operand, used);
}
ExpressionKind::Call(callee, arguments) => {
collect_usages_expression(callee, used);
for argument in arguments {
collect_usages_expression(argument, used);
}
}
ExpressionKind::Field(object, _) => collect_usages_expression(object, used),
ExpressionKind::Index(object, index) => {
collect_usages_expression(object, used);
collect_usages_expression(index, used);
}
ExpressionKind::If {
condition,
then_branch,
else_branch,
} => {
collect_usages_expression(condition, used);
collect_usages_block(then_branch, used);
if let Some(else_block) = else_branch {
collect_usages_block(else_block, used);
}
}
ExpressionKind::Match { value, arms } => {
collect_usages_expression(value, used);
for arm in arms {
collect_usages_block(&arm.body, used);
}
}
ExpressionKind::Block(block) => collect_usages_block(block, used),
ExpressionKind::OpAssign(_, target, value) => {
collect_usages_expression(target, used);
collect_usages_expression(value, used);
}
ExpressionKind::Print(arguments)
| ExpressionKind::ArrayLiteral(arguments)
| ExpressionKind::TupleLiteral(arguments) => {
for argument in arguments {
collect_usages_expression(argument, used);
}
}
ExpressionKind::TypeConstruction(_, fields) => {
for (_, value) in fields {
collect_usages_expression(value, used);
}
}
ExpressionKind::Slice(array, start, end) => {
collect_usages_expression(array, used);
if let Some(start) = start {
collect_usages_expression(start, used);
}
if let Some(end) = end {
collect_usages_expression(end, used);
}
}
_ => {}
}
}
fn lint_dead_labels(block: &Block, errors: &mut Vec<String>) {
let mut labels = HashSet::new();
let mut jumps = HashSet::new();
collect_labels_and_jumps(&block.statements, &mut labels, &mut jumps);
for label in &labels {
if !jumps.contains(label) {
errors.push(format!("dead label '{label}': no jump targets it"));
}
}
}
fn collect_labels_and_jumps(
statements: &[Statement],
labels: &mut HashSet<String>,
jumps: &mut HashSet<String>,
) {
for statement in statements {
match statement {
Statement::Label { name, .. } => {
labels.insert(name.clone());
}
Statement::Jump { label, .. } => {
jumps.insert(label.clone());
}
Statement::Expression(expression) => {
collect_labels_and_jumps_expression(expression, labels, jumps);
}
Statement::Defer(inner) => {
collect_labels_and_jumps(&[*inner.clone()], labels, jumps);
}
_ => {}
}
}
}
fn collect_labels_and_jumps_expression(
expression: &Expression,
labels: &mut HashSet<String>,
jumps: &mut HashSet<String>,
) {
match &expression.kind {
ExpressionKind::If {
then_branch,
else_branch,
..
} => {
collect_labels_and_jumps(&then_branch.statements, labels, jumps);
if let Some(else_block) = else_branch {
collect_labels_and_jumps(&else_block.statements, labels, jumps);
}
}
ExpressionKind::Match { arms, .. } => {
for arm in arms {
collect_labels_and_jumps(&arm.body.statements, labels, jumps);
}
}
ExpressionKind::Block(block) => {
collect_labels_and_jumps(&block.statements, labels, jumps);
}
_ => {}
}
}
fn lint_recursion(function_name: &str, block: &Block, errors: &mut Vec<String>) {
if block_calls_function(block, function_name) {
errors.push(format!(
"recursive call to '{function_name}': use iteration (while/label+jump) instead"
));
}
}
fn block_calls_function(block: &Block, name: &str) -> bool {
block
.statements
.iter()
.any(|statement| statement_calls_function(statement, name))
|| block
.result
.as_ref()
.is_some_and(|result| expression_calls_function(result, name))
}
fn statement_calls_function(statement: &Statement, name: &str) -> bool {
match statement {
Statement::Expression(expression) | Statement::Return(Some(expression)) => {
expression_calls_function(expression, name)
}
Statement::Let { value, .. } | Statement::Assign(_, value) => {
expression_calls_function(value, name)
}
Statement::Label {
initial_arguments, ..
} => initial_arguments
.iter()
.any(|argument| expression_calls_function(argument, name)),
Statement::Jump { arguments, .. } => arguments
.iter()
.any(|argument| expression_calls_function(argument, name)),
Statement::MultiReplace { values, .. } => values
.iter()
.any(|value| expression_calls_function(value, name)),
Statement::Defer(inner) => statement_calls_function(inner, name),
Statement::Return(None) => false,
}
}
fn expression_calls_function(expression: &Expression, name: &str) -> bool {
match &expression.kind {
ExpressionKind::Call(callee, arguments) => {
matches!(&callee.kind, ExpressionKind::Variable(callee_name) if callee_name == name)
|| expression_calls_function(callee, name)
|| arguments
.iter()
.any(|argument| expression_calls_function(argument, name))
}
ExpressionKind::BinaryOperation(_, left, right) | ExpressionKind::Replace(left, right) => {
expression_calls_function(left, name) || expression_calls_function(right, name)
}
ExpressionKind::UnaryOperation(_, operand)
| ExpressionKind::Dereference(operand)
| ExpressionKind::Convert(operand, _)
| ExpressionKind::Transmute(operand, _) => expression_calls_function(operand, name),
ExpressionKind::Field(object, _) => expression_calls_function(object, name),
ExpressionKind::Index(object, index) => {
expression_calls_function(object, name) || expression_calls_function(index, name)
}
ExpressionKind::Block(block) => block_calls_function(block, name),
ExpressionKind::If {
condition,
then_branch,
else_branch,
} => {
expression_calls_function(condition, name)
|| block_calls_function(then_branch, name)
|| else_branch
.as_ref()
.is_some_and(|branch| block_calls_function(branch, name))
}
ExpressionKind::ArrayLiteral(elements) | ExpressionKind::Print(elements) => elements
.iter()
.any(|element| expression_calls_function(element, name)),
ExpressionKind::TypeConstruction(_, fields) => fields
.iter()
.any(|(_, value)| expression_calls_function(value, name)),
ExpressionKind::Slice(array, start, end) => {
expression_calls_function(array, name)
|| start
.as_ref()
.is_some_and(|s| expression_calls_function(s, name))
|| end
.as_ref()
.is_some_and(|e| expression_calls_function(e, name))
}
_ => false,
}
}
fn lint_nesting_depth(block: &Block, depth: usize, max_depth: usize, errors: &mut Vec<String>) {
for statement in &block.statements {
lint_nesting_statement(statement, depth, max_depth, errors);
}
if let Some(result) = &block.result {
lint_nesting_expression(result, depth, max_depth, errors);
}
}
fn lint_nesting_statement(
statement: &Statement,
depth: usize,
max_depth: usize,
errors: &mut Vec<String>,
) {
match statement {
Statement::Expression(expression) | Statement::Return(Some(expression)) => {
lint_nesting_expression(expression, depth, max_depth, errors)
}
Statement::Let { value, .. } => lint_nesting_expression(value, depth, max_depth, errors),
Statement::Defer(inner) => lint_nesting_statement(inner, depth, max_depth, errors),
_ => {}
}
}
fn lint_nesting_expression(
expression: &Expression,
depth: usize,
max_depth: usize,
errors: &mut Vec<String>,
) {
match &expression.kind {
ExpressionKind::If {
condition,
then_branch,
else_branch,
} => {
let new_depth = depth + 1;
if new_depth > max_depth {
errors.push(format!(
"nesting depth {new_depth} exceeds maximum {max_depth}: extract into a function"
));
return;
}
lint_nesting_expression(condition, depth, max_depth, errors);
lint_nesting_depth(then_branch, new_depth, max_depth, errors);
if let Some(else_block) = else_branch {
lint_nesting_depth(else_block, new_depth, max_depth, errors);
}
}
ExpressionKind::Match { value, arms } => {
let new_depth = depth + 1;
if new_depth > max_depth {
errors.push(format!(
"nesting depth {new_depth} exceeds maximum {max_depth}: extract into a function"
));
return;
}
lint_nesting_expression(value, depth, max_depth, errors);
for arm in arms {
lint_nesting_depth(&arm.body, new_depth, max_depth, errors);
}
}
ExpressionKind::Block(block) => lint_nesting_depth(block, depth, max_depth, errors),
_ => {}
}
}
fn lint_unnecessary_return(block: &Block, errors: &mut Vec<String>) {
if block.result.is_some() {
return;
}
if let Some(Statement::Return(Some(_))) = block.statements.last() {
errors.push(
"unnecessary 'return' at end of function — use the expression as block result instead"
.into(),
);
}
if let Some(Statement::Expression(expression)) = block.statements.last() {
check_trailing_return_in_expression(expression, errors);
}
if let Some(result) = &block.result {
check_trailing_return_in_expression(result, errors);
}
}
fn check_trailing_return_in_expression(expression: &Expression, errors: &mut Vec<String>) {
match &expression.kind {
ExpressionKind::If {
then_branch,
else_branch: Some(else_branch),
..
} if block_ends_with_return(then_branch) && block_ends_with_return(else_branch) => {
errors.push(
"unnecessary 'return' in if/else at end of function — use expressions as block results instead"
.into(),
);
}
ExpressionKind::Block(block) if block_ends_with_return(block) => {
errors.push(
"unnecessary 'return' at end of function — use the expression as block result instead"
.into(),
);
}
_ => {}
}
}
fn block_ends_with_return(block: &Block) -> bool {
if block.result.is_some() {
return false;
}
match block.statements.last() {
Some(Statement::Return(Some(_))) => true,
Some(Statement::Expression(expression)) => match &expression.kind {
ExpressionKind::If {
then_branch,
else_branch: Some(else_branch),
..
} => block_ends_with_return(then_branch) && block_ends_with_return(else_branch),
ExpressionKind::Block(inner) => block_ends_with_return(inner),
_ => false,
},
_ => false,
}
}
fn lint_unnecessary_intermediate(block: &Block, errors: &mut Vec<String>) {
if let Some(result) = &block.result
&& let ExpressionKind::Variable(result_name) = &result.kind
&& let Some(Statement::Let {
name: let_name,
binding,
..
}) = block.statements.last()
&& *let_name == *result_name
&& matches!(binding, crate::expression::Binding::Value)
{
errors.push(format!(
"unnecessary intermediate 'let {result_name}' — use the expression directly as block result"
));
}
if let Some(Statement::Return(Some(value))) = block.statements.last()
&& let ExpressionKind::Variable(return_name) = &value.kind
{
let second_last = block
.statements
.len()
.checked_sub(2)
.and_then(|i| block.statements.get(i));
if let Some(Statement::Let {
name: let_name,
binding,
..
}) = second_last
&& *let_name == *return_name
&& matches!(binding, crate::expression::Binding::Value)
{
errors.push(format!(
"unnecessary intermediate 'let {return_name}' — use 'return <expression>' directly"
));
}
}
}