Skip to content

Commit 6d3b138

Browse files
authored
Merge pull request #1048 from codeflash-ai/better-jit-detection
AST parser for detecting common jit decorators
2 parents e27afda + c24f437 commit 6d3b138

2 files changed

Lines changed: 924 additions & 20 deletions

File tree

codeflash/code_utils/line_profile_utils.py

Lines changed: 160 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
import re
5+
import ast
66
from collections import defaultdict
77
from pathlib import Path
88
from typing import TYPE_CHECKING, Union
@@ -16,27 +16,168 @@
1616
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
1717
from codeflash.models.models import CodeOptimizationContext
1818

19-
# Regex pattern to detect JIT compilation decorators from numba, torch, tensorflow, and jax
20-
JIT_DECORATOR_PATTERN = re.compile(
21-
r"@(?:"
22-
# numba decorators
23-
r"(?:numba\.)?(?:jit|njit|vectorize|guvectorize|stencil|cfunc|generated_jit)"
24-
r"|numba\.cuda\.jit"
25-
r"|cuda\.jit"
26-
# torch decorators
27-
r"|torch\.compile"
28-
r"|torch\.jit\.(?:script|trace)"
29-
# tensorflow decorators
30-
r"|(?:tf|tensorflow)\.function"
31-
# jax decorators
32-
r"|jax\.jit"
33-
r")"
34-
)
19+
# Known JIT decorators organized by module
20+
# Format: {module_path: {decorator_name, ...}}
21+
JIT_DECORATORS: dict[str, set[str]] = {
22+
"numba": {"jit", "njit", "vectorize", "guvectorize", "stencil", "cfunc", "generated_jit"},
23+
"numba.cuda": {"jit"},
24+
"torch": {"compile"},
25+
"torch.jit": {"script", "trace"},
26+
"tensorflow": {"function"},
27+
"jax": {"jit"},
28+
}
29+
30+
31+
class JitDecoratorDetector(ast.NodeVisitor):
32+
"""AST visitor that detects JIT compilation decorators considering import aliases."""
33+
34+
def __init__(self) -> None:
35+
# Maps local name -> (module, original_name)
36+
# e.g., {"nb": ("numba", None), "my_jit": ("numba", "jit")}
37+
self.import_aliases: dict[str, tuple[str, str | None]] = {}
38+
self.found_jit_decorator = False
39+
40+
def visit_Import(self, node: ast.Import) -> None:
41+
"""Track regular imports like 'import numba' or 'import numba as nb'."""
42+
for alias in node.names:
43+
# alias.name is the module name, alias.asname is the alias (or None)
44+
local_name = alias.asname if alias.asname else alias.name
45+
# For module imports, we store (module_name, None) to indicate it's a module import
46+
self.import_aliases[local_name] = (alias.name, None)
47+
self.generic_visit(node)
48+
49+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
50+
"""Track from imports like 'from numba import jit' or 'from numba import jit as my_jit'."""
51+
if node.module is None:
52+
self.generic_visit(node)
53+
return
54+
55+
for alias in node.names:
56+
local_name = alias.asname if alias.asname else alias.name
57+
# For from imports, we store (module_name, imported_name)
58+
self.import_aliases[local_name] = (node.module, alias.name)
59+
self.generic_visit(node)
60+
61+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
62+
"""Check function decorators for JIT decorators."""
63+
for decorator in node.decorator_list:
64+
if self._is_jit_decorator(decorator):
65+
self.found_jit_decorator = True
66+
return
67+
self.generic_visit(node)
68+
69+
def _is_jit_decorator(self, node: ast.expr) -> bool:
70+
"""Check if a decorator node is a known JIT decorator."""
71+
# Handle Call nodes (e.g., @jit() or @numba.jit(nopython=True))
72+
if isinstance(node, ast.Call):
73+
return self._is_jit_decorator(node.func)
74+
75+
# Handle simple Name nodes (e.g., @jit when imported directly)
76+
if isinstance(node, ast.Name):
77+
return self._check_name_decorator(node.id)
78+
79+
# Handle Attribute nodes (e.g., @numba.jit or @nb.jit)
80+
if isinstance(node, ast.Attribute):
81+
return self._check_attribute_decorator(node)
82+
83+
return False
84+
85+
def _check_name_decorator(self, name: str) -> bool:
86+
"""Check if a simple name decorator (e.g., @jit) is a JIT decorator."""
87+
if name not in self.import_aliases:
88+
return False
89+
90+
module, imported_name = self.import_aliases[name]
91+
92+
if imported_name is None:
93+
# This is a module import used as decorator (unlikely but possible)
94+
return False
95+
96+
# Check if this is a known JIT decorator from the module
97+
return self._is_known_jit_decorator(module, imported_name)
98+
99+
def _check_attribute_decorator(self, node: ast.Attribute) -> bool:
100+
"""Check if an attribute decorator (e.g., @numba.jit) is a JIT decorator."""
101+
# Build the full attribute chain
102+
parts = self._get_attribute_parts(node)
103+
if not parts:
104+
return False
105+
106+
# The first part might be an alias
107+
first_part = parts[0]
108+
rest_parts = parts[1:]
109+
110+
# Check if first_part is an imported alias
111+
if first_part in self.import_aliases:
112+
module, imported_name = self.import_aliases[first_part]
113+
114+
if imported_name is None:
115+
# It's a module import (e.g., import numba as nb)
116+
# The full path is module + rest_parts
117+
if rest_parts:
118+
full_module = module
119+
decorator_name = rest_parts[-1]
120+
if len(rest_parts) > 1:
121+
full_module = f"{module}.{'.'.join(rest_parts[:-1])}"
122+
return self._is_known_jit_decorator(full_module, decorator_name)
123+
# It's a from import of something that has attributes
124+
# e.g., from torch import jit; @jit.script
125+
elif rest_parts:
126+
full_module = f"{module}.{imported_name}"
127+
decorator_name = rest_parts[-1]
128+
if len(rest_parts) > 1:
129+
full_module = f"{full_module}.{'.'.join(rest_parts[:-1])}"
130+
return self._is_known_jit_decorator(full_module, decorator_name)
131+
# first_part is used directly (e.g., @numba.jit without import alias)
132+
# Reconstruct the full path
133+
elif rest_parts:
134+
full_module = first_part
135+
if len(rest_parts) > 1:
136+
full_module = f"{first_part}.{'.'.join(rest_parts[:-1])}"
137+
decorator_name = rest_parts[-1]
138+
return self._is_known_jit_decorator(full_module, decorator_name)
139+
140+
return False
141+
142+
def _get_attribute_parts(self, node: ast.Attribute) -> list[str]:
143+
"""Get all parts of an attribute chain (e.g., ['numba', 'cuda', 'jit'])."""
144+
parts = []
145+
current = node
146+
147+
while isinstance(current, ast.Attribute):
148+
parts.append(current.attr)
149+
current = current.value
150+
151+
if isinstance(current, ast.Name):
152+
parts.append(current.id)
153+
parts.reverse()
154+
return parts
155+
156+
return []
157+
158+
def _is_known_jit_decorator(self, module: str, decorator_name: str) -> bool:
159+
"""Check if a decorator from a module is a known JIT decorator."""
160+
if module in JIT_DECORATORS:
161+
return decorator_name in JIT_DECORATORS[module]
162+
return False
35163

36164

37165
def contains_jit_decorator(code: str) -> bool:
38-
"""Check if the code contains JIT compilation decorators from numba, torch, tensorflow, or jax."""
39-
return bool(JIT_DECORATOR_PATTERN.search(code))
166+
"""Check if the code contains JIT compilation decorators from numba, torch, tensorflow, or jax.
167+
168+
This function uses AST parsing to accurately detect JIT decorators even when:
169+
- They are imported with aliases (e.g., import numba as nb; @nb.jit)
170+
- They are imported directly (e.g., from numba import jit; @jit)
171+
- They are called with arguments (e.g., @jit(nopython=True))
172+
"""
173+
try:
174+
tree = ast.parse(code)
175+
except SyntaxError:
176+
return False
177+
178+
detector = JitDecoratorDetector()
179+
detector.visit(tree)
180+
return detector.found_jit_decorator
40181

41182

42183
class LineProfilerDecoratorAdder(cst.CSTTransformer):

0 commit comments

Comments
 (0)