Flag direct self-recursion in a hotpath method. Recursion at kHz rates blows the i-cache (200+ cyc for an L2 miss), trashes the RAS predictor (mispredict on every return), and prevents inlining. Only flags **stack** recursion. The following are NOT recursion and are skipped: -
(func_node, fname: str, body, src: bytes, fenced)
| 717 | |
| 718 | |
| 719 | def _recursion_findings(func_node, fname: str, body, src: bytes, fenced) -> list: |
| 720 | """Flag direct self-recursion in a hotpath method. Recursion at kHz |
| 721 | rates blows the i-cache (200+ cyc for an L2 miss), trashes the RAS |
| 722 | predictor (mispredict on every return), and prevents inlining. |
| 723 | |
| 724 | Only flags **stack** recursion. The following are NOT recursion and |
| 725 | are skipped: |
| 726 | - Calls inside a `lambda_expression` body (deferred to whichever |
| 727 | executor consumes the lambda; doesn't grow the stack here). |
| 728 | - Qualified calls like `Base::fname(...)` or `Foo::fname(...)` |
| 729 | (statically dispatched to a different function). |
| 730 | - Method calls on a different object (`other.fname(...)`, |
| 731 | `other->fname(...)`); only bare `fname(...)` and `this->fname(...)` |
| 732 | are real self-calls.""" |
| 733 | if body is None or not fname or fname not in _HOTPATH_METHODS: |
| 734 | return [] |
| 735 | findings: list = [] |
| 736 | seen_lines: set = set() |
| 737 | for node in _walk(body): |
| 738 | if node.type != "call_expression": |
| 739 | continue |
| 740 | # Skip calls that live inside a lambda body -- those execute when |
| 741 | # the lambda runs, not on this call's stack frame. |
| 742 | cur = node.parent |
| 743 | in_lambda = False |
| 744 | while cur is not None and cur is not body: |
| 745 | if cur.type == "lambda_expression": |
| 746 | in_lambda = True |
| 747 | break |
| 748 | cur = cur.parent |
| 749 | if in_lambda: |
| 750 | continue |
| 751 | callee = node.child_by_field_name("function") |
| 752 | if callee is None: |
| 753 | continue |
| 754 | # Recognise: `fname(...)` (identifier) or `this->fname(...)` (field_expression |
| 755 | # whose object is `this`). Reject `Foo::fname`, `obj.fname`, `obj->fname`. |
| 756 | is_self = False |
| 757 | if callee.type == "identifier": |
| 758 | is_self = _node_text(callee, src) == fname |
| 759 | elif callee.type == "field_expression": |
| 760 | obj = callee.child_by_field_name("argument") |
| 761 | field = callee.child_by_field_name("field") |
| 762 | if ( |
| 763 | obj is not None |
| 764 | and field is not None |
| 765 | and obj.type == "this" |
| 766 | and _node_text(field, src) == fname |
| 767 | ): |
| 768 | is_self = True |
| 769 | if not is_self: |
| 770 | continue |
| 771 | line = node.start_point[0] + 1 |
| 772 | if fenced(line) or line in seen_lines: |
| 773 | continue |
| 774 | seen_lines.add(line) |
| 775 | findings.append( |
| 776 | Finding( |
no test coverage detected