Skip to content

Commit 412def2

Browse files
authored
fix: Improve floating point lint to handle ambiguous type (#15133)
Fixes: #14897, #10015 This is not a perfect solution, but it fixes the issue. Happy to discuss further with the clippy team. This PR refines check_mul_add to avoid false positives on ambiguous float types (e.g., f32 vs f64). It improves type detection and ensures mul_add is only suggested when the type is clear. Changes: - Improved detection of ambiguous float types - Refined mul_add suggestion logic - Added tests for ambiguous cases ## Example Before this change, expressions like: ```rust let x = 1.0; // ambiguous type let _ = x * 2.0 + 0.5; // could incorrectly suggest mul_add ``` ---- changelog: [`suboptimal_flops`]: improve handling of ambiguous float types in mul_add suggestions
2 parents 60d5785 + 0c0a4d5 commit 412def2

File tree

7 files changed

+209
-19
lines changed

7 files changed

+209
-19
lines changed

clippy_lints/src/floating_point_arithmetic.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use clippy_utils::consts::Constant::{F32, F64, Int};
22
use clippy_utils::consts::{ConstEvalCtxt, Constant};
33
use clippy_utils::diagnostics::span_lint_and_sugg;
44
use clippy_utils::{
5-
eq_expr_value, get_parent_expr, higher, is_in_const_context, is_inherent_method_call, is_no_std_crate,
6-
numeric_literal, peel_blocks, sugg, sym,
5+
eq_expr_value, get_parent_expr, has_ambiguous_literal_in_expr, higher, is_in_const_context,
6+
is_inherent_method_call, is_no_std_crate, numeric_literal, peel_blocks, sugg, sym,
77
};
88
use rustc_ast::ast;
99
use rustc_errors::Applicability;
@@ -455,7 +455,6 @@ fn is_float_mul_expr<'a>(cx: &LateContext<'_>, expr: &'a Expr<'a>) -> Option<(&'
455455
None
456456
}
457457

458-
// TODO: Fix rust-lang/rust-clippy#4735
459458
fn check_mul_add(cx: &LateContext<'_>, expr: &Expr<'_>) {
460459
if let ExprKind::Binary(
461460
Spanned {
@@ -491,6 +490,14 @@ fn check_mul_add(cx: &LateContext<'_>, expr: &Expr<'_>) {
491490
return;
492491
};
493492

493+
// Check if any variable in the expression has an ambiguous type (could be f32 or f64)
494+
// see: https://github.com/rust-lang/rust-clippy/issues/14897
495+
if (matches!(recv.kind, ExprKind::Path(_)) || matches!(recv.kind, ExprKind::Call(_, _)))
496+
&& has_ambiguous_literal_in_expr(cx, recv)
497+
{
498+
return;
499+
}
500+
494501
span_lint_and_sugg(
495502
cx,
496503
SUBOPTIMAL_FLOPS,

clippy_utils/src/hir_utils.rs

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ use crate::consts::ConstEvalCtxt;
22
use crate::macros::macro_backtrace;
33
use crate::source::{SpanRange, SpanRangeExt, walk_span_to_context};
44
use crate::tokenize_with_text;
5+
use rustc_ast::ast;
56
use rustc_ast::ast::InlineAsmTemplatePiece;
67
use rustc_data_structures::fx::FxHasher;
78
use rustc_hir::MatchSource::TryDesugar;
89
use rustc_hir::def::{DefKind, Res};
910
use rustc_hir::{
1011
AssocItemConstraint, BinOpKind, BindingMode, Block, BodyId, Closure, ConstArg, ConstArgKind, Expr, ExprField,
1112
ExprKind, FnRetTy, GenericArg, GenericArgs, HirId, HirIdMap, InlineAsmOperand, LetExpr, Lifetime, LifetimeKind,
12-
Pat, PatExpr, PatExprKind, PatField, PatKind, Path, PathSegment, PrimTy, QPath, Stmt, StmtKind, StructTailExpr,
13-
TraitBoundModifiers, Ty, TyKind, TyPat, TyPatKind,
13+
Node, Pat, PatExpr, PatExprKind, PatField, PatKind, Path, PathSegment, PrimTy, QPath, Stmt, StmtKind,
14+
StructTailExpr, TraitBoundModifiers, Ty, TyKind, TyPat, TyPatKind,
1415
};
1516
use rustc_lexer::{TokenKind, tokenize};
1617
use rustc_lint::LateContext;
@@ -1004,8 +1005,8 @@ impl<'a, 'tcx> SpanlessHash<'a, 'tcx> {
10041005
self.hash_expr(e);
10051006
}
10061007
},
1007-
ExprKind::Match(e, arms, s) => {
1008-
self.hash_expr(e);
1008+
ExprKind::Match(scrutinee, arms, _) => {
1009+
self.hash_expr(scrutinee);
10091010

10101011
for arm in *arms {
10111012
self.hash_pat(arm.pat);
@@ -1014,8 +1015,6 @@ impl<'a, 'tcx> SpanlessHash<'a, 'tcx> {
10141015
}
10151016
self.hash_expr(arm.body);
10161017
}
1017-
1018-
s.hash(&mut self.s);
10191018
},
10201019
ExprKind::MethodCall(path, receiver, args, _fn_span) => {
10211020
self.hash_name(path.ident.name);
@@ -1058,8 +1057,8 @@ impl<'a, 'tcx> SpanlessHash<'a, 'tcx> {
10581057
ExprKind::Use(expr, _) => {
10591058
self.hash_expr(expr);
10601059
},
1061-
ExprKind::Unary(lop, le) => {
1062-
std::mem::discriminant(lop).hash(&mut self.s);
1060+
ExprKind::Unary(l_op, le) => {
1061+
std::mem::discriminant(l_op).hash(&mut self.s);
10631062
self.hash_expr(le);
10641063
},
10651064
ExprKind::UnsafeBinderCast(kind, expr, ty) => {
@@ -1394,3 +1393,70 @@ fn eq_span_tokens(
13941393
}
13951394
f(cx, left.into_range(), right.into_range(), pred)
13961395
}
1396+
1397+
/// Returns true if the expression contains ambiguous literals (unsuffixed float or int literals)
1398+
/// that could be interpreted as either f32/f64 or i32/i64 depending on context.
1399+
pub fn has_ambiguous_literal_in_expr(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
1400+
match expr.kind {
1401+
ExprKind::Path(ref qpath) => {
1402+
if let Res::Local(hir_id) = cx.qpath_res(qpath, expr.hir_id)
1403+
&& let Node::LetStmt(local) = cx.tcx.parent_hir_node(hir_id)
1404+
&& local.ty.is_none()
1405+
&& let Some(init) = local.init
1406+
{
1407+
return has_ambiguous_literal_in_expr(cx, init);
1408+
}
1409+
false
1410+
},
1411+
ExprKind::Lit(lit) => matches!(
1412+
lit.node,
1413+
ast::LitKind::Float(_, ast::LitFloatType::Unsuffixed) | ast::LitKind::Int(_, ast::LitIntType::Unsuffixed)
1414+
),
1415+
1416+
ExprKind::Array(exprs) | ExprKind::Tup(exprs) => exprs.iter().any(|e| has_ambiguous_literal_in_expr(cx, e)),
1417+
1418+
ExprKind::Assign(lhs, rhs, _) | ExprKind::AssignOp(_, lhs, rhs) | ExprKind::Binary(_, lhs, rhs) => {
1419+
has_ambiguous_literal_in_expr(cx, lhs) || has_ambiguous_literal_in_expr(cx, rhs)
1420+
},
1421+
1422+
ExprKind::Unary(_, e)
1423+
| ExprKind::Cast(e, _)
1424+
| ExprKind::Type(e, _)
1425+
| ExprKind::DropTemps(e)
1426+
| ExprKind::AddrOf(_, _, e)
1427+
| ExprKind::Field(e, _)
1428+
| ExprKind::Index(e, _, _)
1429+
| ExprKind::Yield(e, _) => has_ambiguous_literal_in_expr(cx, e),
1430+
1431+
ExprKind::MethodCall(_, receiver, args, _) | ExprKind::Call(receiver, args) => {
1432+
has_ambiguous_literal_in_expr(cx, receiver) || args.iter().any(|e| has_ambiguous_literal_in_expr(cx, e))
1433+
},
1434+
1435+
ExprKind::Closure(Closure { body, .. }) => {
1436+
let body = cx.tcx.hir_body(*body);
1437+
let closure_expr = crate::peel_blocks(body.value);
1438+
has_ambiguous_literal_in_expr(cx, closure_expr)
1439+
},
1440+
1441+
ExprKind::Block(blk, _) => blk.expr.as_ref().is_some_and(|e| has_ambiguous_literal_in_expr(cx, e)),
1442+
1443+
ExprKind::If(cond, then_expr, else_expr) => {
1444+
has_ambiguous_literal_in_expr(cx, cond)
1445+
|| has_ambiguous_literal_in_expr(cx, then_expr)
1446+
|| else_expr.as_ref().is_some_and(|e| has_ambiguous_literal_in_expr(cx, e))
1447+
},
1448+
1449+
ExprKind::Match(scrutinee, arms, _) => {
1450+
has_ambiguous_literal_in_expr(cx, scrutinee)
1451+
|| arms.iter().any(|arm| has_ambiguous_literal_in_expr(cx, arm.body))
1452+
},
1453+
1454+
ExprKind::Loop(body, ..) => body.expr.is_some_and(|e| has_ambiguous_literal_in_expr(cx, e)),
1455+
1456+
ExprKind::Ret(opt_expr) | ExprKind::Break(_, opt_expr) => {
1457+
opt_expr.as_ref().is_some_and(|e| has_ambiguous_literal_in_expr(cx, e))
1458+
},
1459+
1460+
_ => false,
1461+
}
1462+
}

clippy_utils/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ pub mod visitors;
7777
pub use self::attrs::*;
7878
pub use self::check_proc_macro::{is_from_proc_macro, is_span_if, is_span_match};
7979
pub use self::hir_utils::{
80-
HirEqInterExpr, SpanlessEq, SpanlessHash, both, count_eq, eq_expr_value, hash_expr, hash_stmt, is_bool, over,
80+
HirEqInterExpr, SpanlessEq, SpanlessHash, both, count_eq, eq_expr_value, has_ambiguous_literal_in_expr, hash_expr,
81+
hash_stmt, is_bool, over,
8182
};
8283

8384
use core::mem;

clippy_utils/src/visitors.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ pub fn is_const_evaluatable<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) ->
353353
ExprKind::Binary(_, lhs, rhs)
354354
if self.cx.typeck_results().expr_ty(lhs).peel_refs().is_primitive_ty()
355355
&& self.cx.typeck_results().expr_ty(rhs).peel_refs().is_primitive_ty() => {},
356-
ExprKind::Unary(UnOp::Deref, e) if self.cx.typeck_results().expr_ty(e).is_ref() => (),
356+
ExprKind::Unary(UnOp::Deref, e) if self.cx.typeck_results().expr_ty(e).is_raw_ptr() => (),
357357
ExprKind::Unary(_, e) if self.cx.typeck_results().expr_ty(e).peel_refs().is_primitive_ty() => (),
358358
ExprKind::Index(base, _, _)
359359
if matches!(
@@ -388,7 +388,8 @@ pub fn is_const_evaluatable<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) ->
388388
| ExprKind::Repeat(..)
389389
| ExprKind::Struct(..)
390390
| ExprKind::Tup(_)
391-
| ExprKind::Type(..) => (),
391+
| ExprKind::Type(..)
392+
| ExprKind::UnsafeBinderCast(..) => (),
392393

393394
_ => {
394395
return ControlFlow::Break(());
@@ -676,10 +677,7 @@ pub fn for_each_unconsumed_temporary<'tcx, B>(
676677
helper(typeck, true, else_expr, f)?;
677678
}
678679
},
679-
ExprKind::Type(e, _) => {
680-
helper(typeck, consume, e, f)?;
681-
},
682-
ExprKind::UnsafeBinderCast(_, e, _) => {
680+
ExprKind::Type(e, _) | ExprKind::UnsafeBinderCast(_, e, _) => {
683681
helper(typeck, consume, e, f)?;
684682
},
685683

tests/ui/floating_point_mul_add.fixed

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,47 @@ fn _issue11831() {
6969

7070
let _ = a + b * c;
7171
}
72+
73+
fn _issue14897() {
74+
let x = 1.0;
75+
let _ = x * 2.0 + 0.5; // should not suggest mul_add
76+
let _ = 0.5 + x * 2.0; // should not suggest mul_add
77+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
78+
let _ = 1.2 + x * 1.2; // should not suggest mul_add
79+
80+
let x = -1.0;
81+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
82+
83+
let x = { 4.0 };
84+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
85+
86+
let x = if 1 > 2 { 1.0 } else { 2.0 };
87+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
88+
89+
let x = 2.4 + 1.2;
90+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
91+
92+
let f = || 4.0;
93+
let x = f();
94+
let _ = 0.5 + f() * 1.2; // should not suggest mul_add
95+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
96+
97+
let x = 0.1;
98+
let y = x;
99+
let z = y;
100+
let _ = 0.5 + z * 1.2; // should not suggest mul_add
101+
102+
let _ = 2.0f64.mul_add(x, 0.5);
103+
//~^ suboptimal_flops
104+
let _ = 2.0f64.mul_add(x, 0.5);
105+
//~^ suboptimal_flops
106+
107+
let _ = 2.0f64.mul_add(4.0, x);
108+
//~^ suboptimal_flops
109+
110+
let y: f64 = 1.0;
111+
let _ = y.mul_add(2.0, 0.5);
112+
//~^ suboptimal_flops
113+
let _ = 1.0f64.mul_add(2.0, 0.5);
114+
//~^ suboptimal_flops
115+
}

tests/ui/floating_point_mul_add.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,47 @@ fn _issue11831() {
6969

7070
let _ = a + b * c;
7171
}
72+
73+
fn _issue14897() {
74+
let x = 1.0;
75+
let _ = x * 2.0 + 0.5; // should not suggest mul_add
76+
let _ = 0.5 + x * 2.0; // should not suggest mul_add
77+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
78+
let _ = 1.2 + x * 1.2; // should not suggest mul_add
79+
80+
let x = -1.0;
81+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
82+
83+
let x = { 4.0 };
84+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
85+
86+
let x = if 1 > 2 { 1.0 } else { 2.0 };
87+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
88+
89+
let x = 2.4 + 1.2;
90+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
91+
92+
let f = || 4.0;
93+
let x = f();
94+
let _ = 0.5 + f() * 1.2; // should not suggest mul_add
95+
let _ = 0.5 + x * 1.2; // should not suggest mul_add
96+
97+
let x = 0.1;
98+
let y = x;
99+
let z = y;
100+
let _ = 0.5 + z * 1.2; // should not suggest mul_add
101+
102+
let _ = 0.5 + 2.0 * x;
103+
//~^ suboptimal_flops
104+
let _ = 2.0 * x + 0.5;
105+
//~^ suboptimal_flops
106+
107+
let _ = x + 2.0 * 4.0;
108+
//~^ suboptimal_flops
109+
110+
let y: f64 = 1.0;
111+
let _ = y * 2.0 + 0.5;
112+
//~^ suboptimal_flops
113+
let _ = 1.0 * 2.0 + 0.5;
114+
//~^ suboptimal_flops
115+
}

tests/ui/floating_point_mul_add.stderr

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,35 @@ error: multiply and add expressions can be calculated more efficiently and accur
7979
LL | let _ = a - (b * u as f64);
8080
| ^^^^^^^^^^^^^^^^^^ help: consider using: `b.mul_add(-(u as f64), a)`
8181

82-
error: aborting due to 13 previous errors
82+
error: multiply and add expressions can be calculated more efficiently and accurately
83+
--> tests/ui/floating_point_mul_add.rs:102:13
84+
|
85+
LL | let _ = 0.5 + 2.0 * x;
86+
| ^^^^^^^^^^^^^ help: consider using: `2.0f64.mul_add(x, 0.5)`
87+
88+
error: multiply and add expressions can be calculated more efficiently and accurately
89+
--> tests/ui/floating_point_mul_add.rs:104:13
90+
|
91+
LL | let _ = 2.0 * x + 0.5;
92+
| ^^^^^^^^^^^^^ help: consider using: `2.0f64.mul_add(x, 0.5)`
93+
94+
error: multiply and add expressions can be calculated more efficiently and accurately
95+
--> tests/ui/floating_point_mul_add.rs:107:13
96+
|
97+
LL | let _ = x + 2.0 * 4.0;
98+
| ^^^^^^^^^^^^^ help: consider using: `2.0f64.mul_add(4.0, x)`
99+
100+
error: multiply and add expressions can be calculated more efficiently and accurately
101+
--> tests/ui/floating_point_mul_add.rs:111:13
102+
|
103+
LL | let _ = y * 2.0 + 0.5;
104+
| ^^^^^^^^^^^^^ help: consider using: `y.mul_add(2.0, 0.5)`
105+
106+
error: multiply and add expressions can be calculated more efficiently and accurately
107+
--> tests/ui/floating_point_mul_add.rs:113:13
108+
|
109+
LL | let _ = 1.0 * 2.0 + 0.5;
110+
| ^^^^^^^^^^^^^^^ help: consider using: `1.0f64.mul_add(2.0, 0.5)`
111+
112+
error: aborting due to 18 previous errors
83113

0 commit comments

Comments
 (0)