Skip to content

Commit c24f437

Browse files
authored
Merge branch 'main' into better-jit-detection
2 parents e0b35c7 + e27afda commit c24f437

2 files changed

Lines changed: 1012 additions & 0 deletions

File tree

codeflash/code_utils/code_extractor.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ast
44
import time
55
from dataclasses import dataclass
6+
from importlib.util import find_spec
67
from itertools import chain
78
from pathlib import Path
89
from typing import TYPE_CHECKING, Optional, Union
@@ -1172,6 +1173,198 @@ def get_fn_references_jedi(
11721173
return []
11731174

11741175

1176+
has_numba = find_spec("numba") is not None
1177+
1178+
NUMERICAL_MODULES = frozenset({"numpy", "torch", "numba", "jax", "tensorflow", "math", "scipy"})
1179+
# Modules that require numba to be installed for optimization
1180+
NUMBA_REQUIRED_MODULES = frozenset({"numpy", "math", "scipy"})
1181+
1182+
1183+
class NumericalUsageChecker(ast.NodeVisitor):
1184+
"""AST visitor that checks if a function uses numerical computing libraries."""
1185+
1186+
def __init__(self, numerical_names: set[str]) -> None:
1187+
self.numerical_names = numerical_names
1188+
self.found_numerical = False
1189+
1190+
def visit_Call(self, node: ast.Call) -> None:
1191+
"""Check function calls for numerical library usage."""
1192+
if self.found_numerical:
1193+
return
1194+
call_name = self._get_root_name(node.func)
1195+
if call_name and call_name in self.numerical_names:
1196+
self.found_numerical = True
1197+
return
1198+
self.generic_visit(node)
1199+
1200+
def visit_Attribute(self, node: ast.Attribute) -> None:
1201+
"""Check attribute access for numerical library usage."""
1202+
if self.found_numerical:
1203+
return
1204+
root_name = self._get_root_name(node)
1205+
if root_name and root_name in self.numerical_names:
1206+
self.found_numerical = True
1207+
return
1208+
self.generic_visit(node)
1209+
1210+
def visit_Name(self, node: ast.Name) -> None:
1211+
"""Check name references for numerical library usage."""
1212+
if self.found_numerical:
1213+
return
1214+
if node.id in self.numerical_names:
1215+
self.found_numerical = True
1216+
1217+
def _get_root_name(self, node: ast.expr) -> str | None:
1218+
"""Get the root name from an expression (e.g., 'np' from 'np.array')."""
1219+
if isinstance(node, ast.Name):
1220+
return node.id
1221+
if isinstance(node, ast.Attribute):
1222+
return self._get_root_name(node.value)
1223+
return None
1224+
1225+
1226+
def _collect_numerical_imports(tree: ast.Module) -> tuple[set[str], set[str]]:
1227+
"""Collect names that reference numerical computing libraries from imports.
1228+
1229+
Returns:
1230+
A tuple of (numerical_names, modules_used) where:
1231+
- numerical_names: set of names/aliases that reference numerical libraries
1232+
- modules_used: set of actual module names (e.g., "numpy", "math") being imported
1233+
1234+
"""
1235+
numerical_names: set[str] = set()
1236+
modules_used: set[str] = set()
1237+
1238+
for node in ast.walk(tree):
1239+
if isinstance(node, ast.Import):
1240+
for alias in node.names:
1241+
# import numpy or import numpy as np
1242+
module_root = alias.name.split(".")[0]
1243+
if module_root in NUMERICAL_MODULES:
1244+
# Use the alias if present, otherwise the module name
1245+
name = alias.asname if alias.asname else alias.name.split(".")[0]
1246+
numerical_names.add(name)
1247+
modules_used.add(module_root)
1248+
elif isinstance(node, ast.ImportFrom) and node.module:
1249+
module_root = node.module.split(".")[0]
1250+
if module_root in NUMERICAL_MODULES:
1251+
# from numpy import array, zeros as z
1252+
for alias in node.names:
1253+
if alias.name == "*":
1254+
# Can't track star imports, but mark the module as numerical
1255+
numerical_names.add(module_root)
1256+
else:
1257+
name = alias.asname if alias.asname else alias.name
1258+
numerical_names.add(name)
1259+
modules_used.add(module_root)
1260+
1261+
return numerical_names, modules_used
1262+
1263+
1264+
def _find_function_node(tree: ast.Module, name_parts: list[str]) -> ast.FunctionDef | None:
1265+
"""Find a function node in the AST given its qualified name parts.
1266+
1267+
Note: This function only finds regular (sync) functions, not async functions.
1268+
1269+
Args:
1270+
tree: The parsed AST module
1271+
name_parts: List of name parts, e.g., ["ClassName", "method_name"] or ["function_name"]
1272+
1273+
Returns:
1274+
The function node if found, None otherwise
1275+
1276+
"""
1277+
if not name_parts:
1278+
return None
1279+
1280+
if len(name_parts) == 1:
1281+
# Top-level function
1282+
func_name = name_parts[0]
1283+
for node in tree.body:
1284+
if isinstance(node, ast.FunctionDef) and node.name == func_name:
1285+
return node
1286+
return None
1287+
1288+
if len(name_parts) == 2:
1289+
# Class method: ClassName.method_name
1290+
class_name, method_name = name_parts
1291+
for node in tree.body:
1292+
if isinstance(node, ast.ClassDef) and node.name == class_name:
1293+
for class_node in node.body:
1294+
if isinstance(class_node, ast.FunctionDef) and class_node.name == method_name:
1295+
return class_node
1296+
return None
1297+
1298+
return None
1299+
1300+
1301+
def is_numerical_code(code_string: str, function_name: str) -> bool:
1302+
"""Check if a function uses numerical computing libraries.
1303+
1304+
Detects usage of numpy, torch, numba, jax, tensorflow, scipy, and math libraries
1305+
within the specified function.
1306+
1307+
Note: For math, numpy, and scipy usage, this function returns True only if numba
1308+
is installed in the environment, as numba is required to optimize such code.
1309+
1310+
Args:
1311+
code_string: The entire file's content as a string
1312+
function_name: The name of the function to check. Can be a simple name like "foo"
1313+
or a qualified name like "ClassName.method_name" for methods,
1314+
staticmethods, or classmethods.
1315+
1316+
Returns:
1317+
True if the function uses any numerical computing library functions, False otherwise.
1318+
Returns False for math/numpy/scipy usage if numba is not installed.
1319+
1320+
Examples:
1321+
>>> code = '''
1322+
... import numpy as np
1323+
... def process_data(x):
1324+
... return np.sum(x)
1325+
... '''
1326+
>>> is_numerical_code(code, "process_data") # Returns True only if numba is installed
1327+
True
1328+
1329+
>>> code = '''
1330+
... def simple_func(x):
1331+
... return x + 1
1332+
... '''
1333+
>>> is_numerical_code(code, "simple_func")
1334+
False
1335+
1336+
"""
1337+
try:
1338+
tree = ast.parse(code_string)
1339+
except SyntaxError:
1340+
return False
1341+
1342+
# Split the function name to handle class methods
1343+
name_parts = function_name.split(".")
1344+
1345+
# Find the target function node
1346+
target_function = _find_function_node(tree, name_parts)
1347+
if target_function is None:
1348+
return False
1349+
1350+
# Collect names that reference numerical modules from imports
1351+
numerical_names, modules_used = _collect_numerical_imports(tree)
1352+
1353+
# Check if the function body uses any numerical library
1354+
checker = NumericalUsageChecker(numerical_names)
1355+
checker.visit(target_function)
1356+
1357+
if not checker.found_numerical:
1358+
return False
1359+
1360+
# If numba is not installed and all modules used require numba for optimization,
1361+
# return False since we can't optimize this code
1362+
if not has_numba and modules_used.issubset(NUMBA_REQUIRED_MODULES): # noqa : SIM103
1363+
return False
1364+
1365+
return True
1366+
1367+
11751368
def get_opt_review_metrics(
11761369
source_code: str, file_path: Path, qualified_name: str, project_root: Path, tests_root: Path
11771370
) -> str:

0 commit comments

Comments
 (0)