Skip to content

Adding Passes

Dianel edited this page Apr 9, 2026 · 1 revision

Adding New Optimization Passes

GraphOptim is designed to be extensible. This guide walks you through adding a new optimization pass.

Pass Architecture

Every pass implements two methods:

  • detect(source_code, func_name?) → list[Finding] — Find issues
  • fix(source_code) → str — Apply fixes (must return valid Python)

And two attributes for the knapsack selector:

  • cost: float — Risk level (0.0 = safe, 1.0 = dangerous)
  • benefit: float — Set dynamically during detect() based on findings

Step-by-Step Guide

1. Create the Pass Module

# graphoptim/optimizer/passes/variable_shadowing.py

"""
Variable Shadowing Detection pass for GraphOptim.

Detects inner scope variables that shadow outer scope names,
a common pattern in AI-generated code.
"""

from __future__ import annotations

import ast
from dataclasses import dataclass
from typing import Optional


@dataclass
class ShadowingFinding:
    """A variable shadowing finding."""
    inner_name: str
    inner_line: Optional[int]
    outer_line: Optional[int]
    description: str


class VariableShadowingPass:
    """
    Detects and suggests fixes for variable shadowing.

    Detection: Inner scope variable names that match outer scope names.
    Fix: Rename inner variables to avoid shadowing (advisory via comments).
    """

    name = "variable_shadowing"
    cost = 0.4   # Medium risk — renaming variables
    benefit = 0.0  # Set dynamically

    def detect(
        self, source_code: str, func_name: Optional[str] = None
    ) -> list[ShadowingFinding]:
        """Find shadowed variables."""
        findings = []
        # ... detection logic ...

        if findings:
            self.benefit = min(len(findings) * 0.1, 0.5)
        return findings

    def fix(self, source_code: str) -> str:
        """Apply fixes. Must return valid Python."""
        findings = self.detect(source_code)
        if not findings:
            return source_code

        # Apply transformation...
        # CRITICAL: validate output
        import ast
        result = source_code  # ... transform ...
        ast.parse(result)  # Must not raise
        return result

2. Register in the Rewriter

Edit graphoptim/optimizer/rewriter.py:

from graphoptim.optimizer.passes.variable_shadowing import VariableShadowingPass

PASS_REGISTRY: dict[str, type] = {
    "dead_code": DeadCodePass,
    "path_shortener": PathShortenerPass,
    "centrality": CentralityPass,
    "variable_shadowing": VariableShadowingPass,  # ADD THIS
}

3. Write Tests

Create tests/test_variable_shadowing.py:

"""Tests for variable shadowing detection."""

import ast
from graphoptim.optimizer.passes.variable_shadowing import VariableShadowingPass


class TestVariableShadowingDetection:
    def test_inner_shadows_outer(self):
        code = '''
def foo():
    x = 1
    def bar():
        x = 2  # Shadows outer x
        return x
    return bar()
'''
        pass_ = VariableShadowingPass()
        findings = pass_.detect(code)
        assert len(findings) >= 1

    def test_no_shadowing(self):
        code = '''
def foo():
    x = 1
    return x
'''
        pass_ = VariableShadowingPass()
        findings = pass_.detect(code)
        assert len(findings) == 0


class TestVariableShadowingFix:
    def test_output_is_valid_python(self):
        code = '''
def foo():
    x = 1
    def bar():
        x = 2
        return x
    return bar()
'''
        pass_ = VariableShadowingPass()
        result = pass_.fix(code)
        ast.parse(result)  # Must not raise

    def test_preserves_semantics(self):
        """Fix should not change observable behavior."""
        ...

4. Update Documentation

README.md — Add to the passes table:

| `variable_shadowing` | Inner scope shadows outer variables | Renames or adds advisory comments |

CHANGELOG.md — Add under [Unreleased]:

### Added
- Variable shadowing detection and advisory pass

Design Rules

  1. Never break working code — If unsure, skip the fix and emit a warning
  2. Validate with ast.parse() — Every fix() output MUST parse
  3. Set benefit dynamically — Based on what detect() actually finds
  4. Be conservative with cost — Automated refactoring is risky
  5. Preserve comments and docstrings — Use AST transformations carefully
  6. Handle edge cases — Async functions, decorators, nested classes

Knapsack Integration

Your pass automatically participates in knapsack selection. The selector will:

  1. Call detect() to set benefit dynamically
  2. Use cost and benefit to decide inclusion
  3. Respect prerequisites if your pass depends on others
class MyPass:
    name = "my_pass"
    cost = 0.3
    benefit = 0.0  # Updated by detect()
    prerequisites = ["dead_code"]  # Optional: must run after dead_code

Clone this wiki locally