EcoCode / utils /code_analysis.py
Disha-Singh-Singraur's picture
fix: use 0.01/0.99 bounds — 0.001/0.999 printed as 0.00/1.00 with 2dp format
8b2c82d
Raw
History Blame Contribute Delete
7.93 kB
"""AST-based proxy analysis for code quality metrics.
Fully deterministic — no runtime benchmarking. Analyses the code's
abstract syntax tree to estimate optimization quality.
"""
import ast
from typing import Any, Dict
# Scores must be strictly within (0, 1) per validator
# 0.01/0.99 are safe with :.2f formatting (0.001 → "0.00", 0.999 → "1.00" breaks it)
_SCORE_MIN = 0.01
_SCORE_MAX = 0.99
# Builtins / methods that indicate good Python style
EFFICIENT_BUILTINS = frozenset({
"sum", "map", "filter", "sorted", "enumerate",
"zip", "any", "all", "min", "max", "len",
})
EFFICIENT_STR_METHODS = frozenset({
"join", "capitalize", "title", "upper", "lower",
"strip", "replace", "split",
})
def analyze_code(code: str) -> Dict[str, Any]:
"""Analyse Python source and return deterministic proxy metrics.
Returns a dict with:
- loop_count: number of for/while loops
- max_nesting_depth: deepest loop nesting
- list_comprehension_count: number of list/set/dict comprehensions
- generator_expression_count: number of genexprs
- builtin_usage_count: uses of efficient builtins
- str_method_count: uses of efficient string methods
- redundant_variable_count: estimated unused/trivial assignments
- total_lines: non-blank source lines
- has_index_loop: True if `range(len(...))` pattern detected
"""
try:
tree = ast.parse(code)
except SyntaxError:
return _empty_analysis()
metrics: Dict[str, Any] = {
"loop_count": 0,
"max_nesting_depth": 0,
"list_comprehension_count": 0,
"generator_expression_count": 0,
"builtin_usage_count": 0,
"str_method_count": 0,
"redundant_variable_count": 0,
"total_lines": _count_lines(code),
"has_index_loop": False,
}
_walk(tree, metrics, depth=0)
metrics["redundant_variable_count"] = _count_redundant_vars(tree)
return metrics
def _empty_analysis() -> Dict[str, Any]:
return {
"loop_count": 0,
"max_nesting_depth": 0,
"list_comprehension_count": 0,
"generator_expression_count": 0,
"builtin_usage_count": 0,
"str_method_count": 0,
"redundant_variable_count": 0,
"total_lines": 0,
"has_index_loop": False,
}
def _count_lines(code: str) -> int:
return sum(1 for line in code.splitlines() if line.strip())
def _walk(node: ast.AST, metrics: Dict[str, Any], depth: int) -> None:
"""Recursive AST walk tracking nesting depth."""
if isinstance(node, (ast.For, ast.While)):
metrics["loop_count"] += 1
new_depth = depth + 1
metrics["max_nesting_depth"] = max(metrics["max_nesting_depth"], new_depth)
# Detect range(len(...)) pattern
if isinstance(node, ast.For) and _is_range_len(node.iter):
metrics["has_index_loop"] = True
for child in ast.iter_child_nodes(node):
_walk(child, metrics, new_depth)
return
if isinstance(node, ast.ListComp):
metrics["list_comprehension_count"] += 1
elif isinstance(node, (ast.SetComp, ast.DictComp)):
metrics["list_comprehension_count"] += 1
elif isinstance(node, ast.GeneratorExp):
metrics["generator_expression_count"] += 1
# Detect efficient builtin calls
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id in EFFICIENT_BUILTINS:
metrics["builtin_usage_count"] += 1
# Detect str method calls like ",".join(...)
if isinstance(node.func, ast.Attribute):
if node.func.attr in EFFICIENT_STR_METHODS:
metrics["str_method_count"] += 1
for child in ast.iter_child_nodes(node):
_walk(child, metrics, depth)
def _is_range_len(node: ast.AST) -> bool:
"""Detect `range(len(...))` pattern."""
if not isinstance(node, ast.Call):
return False
if not (isinstance(node.func, ast.Name) and node.func.id == "range"):
return False
if len(node.args) != 1:
return False
arg = node.args[0]
return (
isinstance(arg, ast.Call)
and isinstance(arg.func, ast.Name)
and arg.func.id == "len"
)
def _count_redundant_vars(tree: ast.AST) -> int:
"""Heuristic: count variables assigned but only read once or never."""
assignments: Dict[str, int] = {}
reads: Dict[str, int] = {}
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
assignments[target.id] = assignments.get(target.id, 0) + 1
elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
reads[node.id] = reads.get(node.id, 0) + 1
# A variable is "redundant" if it is assigned but read ≤ 1 time
# and is not the function name or 'return' target
redundant = 0
for var, assign_count in assignments.items():
read_count = reads.get(var, 0)
if assign_count >= 1 and read_count <= 1:
redundant += 1
return redundant
def compute_improvement_score(
original_metrics: Dict[str, Any],
new_metrics: Dict[str, Any],
) -> float:
"""Compute a 0.0–1.0 score comparing new code against original.
Higher = better optimization. Fully deterministic.
"""
score = 0.0
max_possible = 0.0
# 1. Reduced loop count (weight 0.20)
max_possible += 0.20
orig_loops = original_metrics["loop_count"]
new_loops = new_metrics["loop_count"]
if orig_loops > 0 and new_loops < orig_loops:
score += 0.20 * (1.0 - new_loops / orig_loops)
# 2. Reduced nesting depth (weight 0.15)
max_possible += 0.15
orig_depth = original_metrics["max_nesting_depth"]
new_depth = new_metrics["max_nesting_depth"]
if orig_depth > 0 and new_depth < orig_depth:
score += 0.15 * (1.0 - new_depth / orig_depth)
# 3. Use of comprehensions (weight 0.15)
max_possible += 0.15
if new_metrics["list_comprehension_count"] > original_metrics["list_comprehension_count"]:
score += 0.15
if new_metrics["generator_expression_count"] > original_metrics["generator_expression_count"]:
score += 0.05
max_possible += 0.05
# 4. Use of efficient builtins (weight 0.20)
max_possible += 0.20
if new_metrics["builtin_usage_count"] > original_metrics["builtin_usage_count"]:
gained = new_metrics["builtin_usage_count"] - original_metrics["builtin_usage_count"]
score += min(0.20, 0.05 * gained)
# 5. Use of efficient string methods (weight 0.10)
max_possible += 0.10
if new_metrics["str_method_count"] > original_metrics["str_method_count"]:
score += 0.10
# 6. Removed index-based loops (weight 0.10)
max_possible += 0.10
if original_metrics["has_index_loop"] and not new_metrics["has_index_loop"]:
score += 0.10
# 7. Reduced redundant variables (weight 0.10)
max_possible += 0.10
orig_red = original_metrics["redundant_variable_count"]
new_red = new_metrics["redundant_variable_count"]
if orig_red > 0 and new_red < orig_red:
score += 0.10 * (1.0 - new_red / orig_red)
# Normalize to strictly (0, 1) per validator requirement
if max_possible > 0:
raw = min(1.0, score / max_possible * 1.25) # slight scale factor
return max(_SCORE_MIN, min(_SCORE_MAX, raw))
return _SCORE_MIN
def metrics_are_equal(a: Dict[str, Any], b: Dict[str, Any]) -> bool:
"""Check if two metric dicts are identical (for plateau detection)."""
keys = [
"loop_count", "max_nesting_depth", "list_comprehension_count",
"generator_expression_count", "builtin_usage_count",
"str_method_count", "redundant_variable_count", "has_index_loop",
]
return all(a.get(k) == b.get(k) for k in keys)