diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 00000000..278f0212 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-06-18 - AST Traversal Performance +**Learning:** For hot-path AST traversal, eager list-appending (`list.append()`) is consistently faster than `yield from` recursion. +**Action:** Use list-appending instead of `yield from` for AST traversal to improve performance. diff --git a/src/wardline/scanner/ast_primitives.py b/src/wardline/scanner/ast_primitives.py index 70f565b3..6a092e8b 100644 --- a/src/wardline/scanner/ast_primitives.py +++ b/src/wardline/scanner/ast_primitives.py @@ -105,38 +105,44 @@ def iter_calls_in_function_body( values, base classes, metaclass keywords) are still attributed to ``node``. """ - def walk_node(current: ast.AST) -> Iterator[ast.Call]: + # Optimization: Eager list appending is significantly faster than recursive 'yield from' + # in CPython due to reduced generator delegation overhead on deep AST paths. + result: list[ast.Call] = [] + + def walk_node(current: ast.AST) -> None: if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef)): for decorator in current.decorator_list: - yield from walk_node(decorator) - yield from _walk_argument_defaults(current.args) + walk_node(decorator) + _walk_argument_defaults(current.args) return if isinstance(current, ast.ClassDef): for decorator in current.decorator_list: - yield from walk_node(decorator) + walk_node(decorator) for base in current.bases: - yield from walk_node(base) + walk_node(base) for keyword in current.keywords: - yield from walk_node(keyword.value) + walk_node(keyword.value) return if isinstance(current, ast.Lambda): - yield from _walk_argument_defaults(current.args) + _walk_argument_defaults(current.args) return if isinstance(current, ast.Call): - yield current + result.append(current) for child in ast.iter_child_nodes(current): - yield from walk_node(child) + walk_node(child) - def _walk_argument_defaults(args: ast.arguments) -> Iterator[ast.Call]: + def _walk_argument_defaults(args: ast.arguments) -> None: for default in args.defaults: - yield from walk_node(default) + walk_node(default) for kw_default in args.kw_defaults: if kw_default is None: continue - yield from walk_node(kw_default) + walk_node(kw_default) for stmt in node.body: - yield from walk_node(stmt) + walk_node(stmt) + + return iter(result) def resolve_self_method_fqn(