Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

`shortCircuitCondition' detector #202

Merged
merged 11 commits into from
Nov 1, 2024
115 changes: 115 additions & 0 deletions src/detectors/builtin/shortCircuitCondition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { CompilationUnit } from "../../internals/ir";
import {
evalsToValue,
foldStatements,
forEachExpression,
} from "../../internals/tact";
import { MistiTactWarning, Severity } from "../../internals/warnings";
import { ASTDetector } from "../detector";
import {
AstExpression,
AstMethodCall,
} from "@tact-lang/compiler/dist/grammar/ast";

/**
* A detector that suggests optimizing boolean expressions to leverage short-circuit evaluation.
*
* ## Why is it bad?
* TVM supports short-circuit operations. When using logical AND (&&) operations,
Esorat marked this conversation as resolved.
Show resolved Hide resolved
* placing constant or cheaper conditions first can prevent unnecessary execution
* of expensive operations when the result is already determined.
*
* ## Example
* ```tact
* // Bad: Expensive operation is always executed
* if (expensive_function() && constant_false) {
* // ...
* }
* ```
*
* Use instead:
* ```tact
* // Good: Expensive operation is skipped when constant_false is false
* if (constant_false && expensive_function()) {
* // ...
* }
* ```
*/
export class ShortCircuitCondition extends ASTDetector {
severity = Severity.LOW;

async check(cu: CompilationUnit): Promise<MistiTactWarning[]> {
return Array.from(cu.ast.getFunctions()).reduce(
(acc, fun) => acc.concat(this.checkFunction(fun)),
[] as MistiTactWarning[],
);
}

private checkFunction(fun: any): MistiTactWarning[] {
return foldStatements(
fun,
(acc, stmt) => {
forEachExpression(stmt, (expr) => {
if (
expr.kind === "op_binary" &&
(expr.op === "&&" || expr.op === "||")
) {
const leftConst = this.isConstantExpression(expr.left);
Esorat marked this conversation as resolved.
Show resolved Hide resolved
const rightConst = this.isConstantExpression(expr.right);
const leftExpensive = this.isExpensiveFunction(expr.left);
if (
(expr.op === "&&" && !leftConst && rightConst) ||
(expr.op === "||" && leftConst && !rightConst)
) {
acc.push(
this.makeWarning(
`Consider reordering: ${
leftExpensive
? "Move expensive function call to the end."
: "Move constant to the left."
}`,
expr.loc,
{
suggestion: `Reorder to optimize ${expr.op} condition short-circuiting`,
Esorat marked this conversation as resolved.
Show resolved Hide resolved
},
),
);
}
}
});
return acc;
},
[] as MistiTactWarning[],
);
}

private isExpensiveFunction(expr: AstExpression): boolean {
if (expr.kind === "method_call") {
Esorat marked this conversation as resolved.
Show resolved Hide resolved
if (expr.args.length > 2) return true;
return this.hasNestedCallsOrRecursion(expr);
}
return false;
}

private hasNestedCallsOrRecursion(expr: AstMethodCall): boolean {
Esorat marked this conversation as resolved.
Show resolved Hide resolved
for (const arg of expr.args) {
if (
arg.kind === "method_call" ||
(arg.kind === "id" && arg.text === expr.method.text)
) {
return true;
}
}
return false;
}

private isConstantExpression(expr: AstExpression): boolean {
return (
evalsToValue(expr, "boolean", true) ||
evalsToValue(expr, "boolean", false) ||
expr.kind === "boolean" ||
(expr.kind === "op_binary" &&
["==", "!=", ">", "<", ">=", "<="].includes(expr.op))
Esorat marked this conversation as resolved.
Show resolved Hide resolved
);
}
}
7 changes: 7 additions & 0 deletions src/detectors/detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,13 @@ export const BuiltInDetectors: Record<string, DetectorEntry> = {
),
enabledByDefault: true,
},
ShortCircuitCondition: {
loader: (ctx: MistiContext) =>
import("./builtin/shortCircuitCondition").then(
(module) => new module.ShortCircuitCondition(ctx),
),
enabledByDefault: true,
},
};

/**
Expand Down
35 changes: 35 additions & 0 deletions test/detectors/ShortCircuitCondition.expected.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[LOW] ShortCircuitCondition: Consider reordering: Move constant to the left.
test/detectors/ShortCircuitCondition.tact:6:17:
5 | fun testCondition1(): Bool {
> 6 | return (self.expensiveCheck() && (self.a > 10)); // Bad
^
7 | }
Help: Reorder to optimize && condition short-circuiting
See: https://nowarp.io/tools/misti/docs/detectors/ShortCircuitCondition

[LOW] ShortCircuitCondition: Consider reordering: Move constant to the left.
test/detectors/ShortCircuitCondition.tact:11:17:
10 | fun testCondition2(): Bool {
> 11 | return (true || self.expensiveCheck()); // Bad
^
12 | }
Help: Reorder to optimize || condition short-circuiting
See: https://nowarp.io/tools/misti/docs/detectors/ShortCircuitCondition

[LOW] ShortCircuitCondition: Consider reordering: Move constant to the left.
test/detectors/ShortCircuitCondition.tact:16:17:
15 | fun testCondition3(): Bool {
> 16 | return ((self.a > 0) && self.expensiveCheck() && false); //Bad: Should warn for moving false left
^
17 | }
Help: Reorder to optimize && condition short-circuiting
See: https://nowarp.io/tools/misti/docs/detectors/ShortCircuitCondition

[LOW] ShortCircuitCondition: Consider reordering: Move constant to the left.
test/detectors/ShortCircuitCondition.tact:21:14:
20 | fun testCondition4(): Bool {
> 21 | if ((self.expensiveCheck() && (self.a < 3)) || ((self.a > 10) && true)) {
^
22 | return true; // Bad: Should warn for moving constants to the left
Help: Reorder to optimize && condition short-circuiting
See: https://nowarp.io/tools/misti/docs/detectors/ShortCircuitCondition
35 changes: 35 additions & 0 deletions test/detectors/ShortCircuitCondition.tact
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
contract ShortCircuitTest {
a: Int = 5;

// Test 1: Unoptimized && condition, should warn to reorder
fun testCondition1(): Bool {
return (self.expensiveCheck() && (self.a > 10)); // Bad
}

// Test 2: Unoptimized || condition, should warn to reorder
fun testCondition2(): Bool {
return (true || self.expensiveCheck()); // Bad
}

// Test 3: Complex condition with both optimized and unoptimized parts
fun testCondition3(): Bool {
return ((self.a > 0) && self.expensiveCheck() && false); //Bad: Should warn for moving false left
}

// Test 4: Nested conditions
fun testCondition4(): Bool {
if ((self.expensiveCheck() && (self.a < 3)) || ((self.a > 10) && true)) {
return true; // Bad: Should warn for moving constants to the left
}
return false;
}

// Test 5: No optimization needed, should not warn
fun testCondition5(): Bool {
return (self.a > 0 && self.expensiveCheck()); // Ok
}

fun expensiveCheck(): Bool {
return true;
}
}
Loading