mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 00:45:38 +00:00
Analyzer: W0101 warn on expensive multiply/divide
Emits W0101 on \`a * b\`, \`a / b\`, \`a % b\` expressions where both operands are non-constant (neither is an integer literal). These lower to calls into the software multiply/divide routines and can blow the 6502 cycle budget. Multiplying by a constant is still silent because the optimizer can strength-reduce power-of-2 cases to shifts and small factors to add chains. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
4e05c008ce
commit
1e4e384501
3 changed files with 67 additions and 2 deletions
|
|
@ -255,7 +255,34 @@ impl Analyzer {
|
|||
}
|
||||
self.walk_expr_reads(idx);
|
||||
}
|
||||
Expr::BinaryOp(lhs, _, rhs, _) => {
|
||||
Expr::BinaryOp(lhs, op, rhs, span) => {
|
||||
// W0101: warn about multiply/divide/modulo with a non-
|
||||
// constant operand. These lower to calls into the
|
||||
// software multiply/divide routines, which are far more
|
||||
// expensive than the simple inline opcodes used for
|
||||
// add/sub. A literal like `x * 2` can be strength-
|
||||
// reduced to a shift and is therefore cheap.
|
||||
if matches!(op, BinOp::Mul | BinOp::Div | BinOp::Mod)
|
||||
&& !is_small_constant(lhs)
|
||||
&& !is_small_constant(rhs)
|
||||
{
|
||||
let op_name = match op {
|
||||
BinOp::Mul => "multiply",
|
||||
BinOp::Div => "divide",
|
||||
BinOp::Mod => "modulo",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
self.diagnostics.push(
|
||||
Diagnostic::warning(
|
||||
ErrorCode::W0101,
|
||||
format!("{op_name} with two non-constant operands is expensive"),
|
||||
*span,
|
||||
)
|
||||
.with_help(
|
||||
"consider precomputing or using a power-of-2 constant for strength reduction",
|
||||
),
|
||||
);
|
||||
}
|
||||
self.walk_expr_reads(lhs);
|
||||
self.walk_expr_reads(rhs);
|
||||
}
|
||||
|
|
@ -930,6 +957,13 @@ fn collect_calls_block(block: &Block, calls: &mut Vec<String>) {
|
|||
/// unbounded `loop { }` is actually an infinite spin. We recurse into
|
||||
/// nested control-flow blocks so that a `break` inside a conditional
|
||||
/// body still counts as "can exit".
|
||||
/// True if the expression is a small integer literal — used to avoid
|
||||
/// emitting W0101 for multiply/divide where at least one operand can be
|
||||
/// handled by strength reduction (e.g. `x * 2`, `x / 4`).
|
||||
fn is_small_constant(expr: &Expr) -> bool {
|
||||
matches!(expr, Expr::IntLiteral(_, _))
|
||||
}
|
||||
|
||||
fn block_can_exit_or_yield(block: &Block) -> bool {
|
||||
block.statements.iter().any(stmt_can_exit_or_yield)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,6 +298,38 @@ fn analyze_return_wrong_type() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_expensive_multiply_warns() {
|
||||
let errors = analyze_errors(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
var a: u8 = 3
|
||||
var b: u8 = 5
|
||||
var c: u8 = 0
|
||||
on frame { c = a * b }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
assert!(
|
||||
errors.contains(&ErrorCode::W0101),
|
||||
"variable*variable multiply should emit W0101, got: {errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_multiply_by_constant_ok() {
|
||||
// Multiply by a literal is cheap (strength reduced to shifts).
|
||||
analyze_ok(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
var a: u8 = 3
|
||||
var c: u8 = 0
|
||||
on frame { c = a * 4 }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_on_scanline_requires_mmc3() {
|
||||
let errors = analyze_errors(
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ pub enum ErrorCode {
|
|||
E0505, // multiple start declarations
|
||||
|
||||
// W01xx: Warnings
|
||||
#[allow(dead_code)]
|
||||
W0101, // expensive multiply/divide operation
|
||||
W0102, // loop without break or wait_frame
|
||||
W0103, // unused variable
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue