|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | | -import re |
| 5 | +import ast |
6 | 6 | from collections import defaultdict |
7 | 7 | from pathlib import Path |
8 | 8 | from typing import TYPE_CHECKING, Union |
|
16 | 16 | from codeflash.discovery.functions_to_optimize import FunctionToOptimize |
17 | 17 | from codeflash.models.models import CodeOptimizationContext |
18 | 18 |
|
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 |
35 | 163 |
|
36 | 164 |
|
37 | 165 | 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 |
40 | 181 |
|
41 | 182 |
|
42 | 183 | class LineProfilerDecoratorAdder(cst.CSTTransformer): |
|
0 commit comments