code-towel 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_towel-1.0.0.dist-info/METADATA +722 -0
- code_towel-1.0.0.dist-info/RECORD +27 -0
- code_towel-1.0.0.dist-info/WHEEL +5 -0
- code_towel-1.0.0.dist-info/entry_points.txt +4 -0
- code_towel-1.0.0.dist-info/licenses/LICENSE +201 -0
- code_towel-1.0.0.dist-info/top_level.txt +1 -0
- towel/__init__.py +20 -0
- towel/cli.py +813 -0
- towel/unification/__init__.py +24 -0
- towel/unification/assignment_analyzer.py +403 -0
- towel/unification/ast_normalizer.py +223 -0
- towel/unification/ast_pretty_printer.py +133 -0
- towel/unification/binding_detector.py +381 -0
- towel/unification/block_signature.py +123 -0
- towel/unification/builtins.py +209 -0
- towel/unification/exceptions.py +113 -0
- towel/unification/extractor.py +884 -0
- towel/unification/models.py +190 -0
- towel/unification/nominal_unifier.py +375 -0
- towel/unification/orphan_detector.py +187 -0
- towel/unification/pipeline.py +524 -0
- towel/unification/project_layout.py +201 -0
- towel/unification/refactor_engine.py +3381 -0
- towel/unification/scope_analyzer.py +597 -0
- towel/unification/unifier.py +2128 -0
- towel/unification/visitor_utils.py +83 -0
- towel/unification/visitors.py +377 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Copyright 2025 Eric Allen
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Data models for the unification-based refactoring system.
|
|
16
|
+
|
|
17
|
+
This module defines the core data structures used throughout the refactoring engine:
|
|
18
|
+
- Code block pairs for comparison
|
|
19
|
+
- Method information for class context
|
|
20
|
+
- Refactoring proposals
|
|
21
|
+
- Function context for analysis
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import TYPE_CHECKING, List, Literal, Optional, Tuple, Union
|
|
27
|
+
import ast
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CodeBlockPair:
|
|
32
|
+
"""Represents a pair of potentially duplicate code blocks."""
|
|
33
|
+
|
|
34
|
+
file_path: str
|
|
35
|
+
function1_name: str
|
|
36
|
+
function2_name: str
|
|
37
|
+
block1_range: Tuple[int, int]
|
|
38
|
+
block2_range: Tuple[int, int]
|
|
39
|
+
block1_nodes: List[ast.AST]
|
|
40
|
+
block2_nodes: List[ast.AST]
|
|
41
|
+
file_path2: Optional[str] = None
|
|
42
|
+
class1_name: Optional[str] = None
|
|
43
|
+
class2_name: Optional[str] = None
|
|
44
|
+
enclosing_function1_name: Optional[str] = None
|
|
45
|
+
enclosing_function2_name: Optional[str] = None
|
|
46
|
+
function1_ancestry: Optional[List[str]] = None
|
|
47
|
+
function2_ancestry: Optional[List[str]] = None
|
|
48
|
+
scope_analyzer1: Optional["ScopeAnalyzer"] = None
|
|
49
|
+
scope_analyzer2: Optional["ScopeAnalyzer"] = None
|
|
50
|
+
root_scope1: Optional["Scope"] = None
|
|
51
|
+
root_scope2: Optional["Scope"] = None
|
|
52
|
+
source1: Optional[str] = None
|
|
53
|
+
source2: Optional[str] = None
|
|
54
|
+
function1_node: Optional[Union[ast.FunctionDef, ast.AsyncFunctionDef]] = None
|
|
55
|
+
function2_node: Optional[Union[ast.FunctionDef, ast.AsyncFunctionDef]] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class MethodInfo:
|
|
60
|
+
"""Describes how a function participates as a method within a class."""
|
|
61
|
+
|
|
62
|
+
kind: Optional[Literal["instance", "classmethod", "staticmethod"]]
|
|
63
|
+
implicit_param: Optional[str]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class ClassInfo:
|
|
68
|
+
"""Summarizes class definitions discovered during analysis."""
|
|
69
|
+
|
|
70
|
+
name: str
|
|
71
|
+
qualname: str
|
|
72
|
+
file_path: str
|
|
73
|
+
bases: List[str]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class ClassInsertionPlan:
|
|
78
|
+
"""Describes where an extracted helper should be inserted within a class hierarchy."""
|
|
79
|
+
|
|
80
|
+
class_name: str
|
|
81
|
+
file_path: str
|
|
82
|
+
method_kind: Literal["instance", "classmethod", "staticmethod"]
|
|
83
|
+
implicit_param: Optional[str]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Replacement:
|
|
88
|
+
"""Represents a replacement call to the extracted function/method."""
|
|
89
|
+
|
|
90
|
+
line_range: Tuple[int, int]
|
|
91
|
+
node: ast.AST
|
|
92
|
+
file_path: Optional[str] = None
|
|
93
|
+
class_name: Optional[str] = None
|
|
94
|
+
method_kind: Optional[Literal["instance", "classmethod", "staticmethod"]] = None
|
|
95
|
+
implicit_param: Optional[str] = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class RefactoringProposal:
|
|
100
|
+
"""Proposed refactoring."""
|
|
101
|
+
|
|
102
|
+
file_path: str
|
|
103
|
+
extracted_function: ast.FunctionDef
|
|
104
|
+
replacements: List[Replacement]
|
|
105
|
+
description: str
|
|
106
|
+
parameters_count: int
|
|
107
|
+
return_variables: List[str] = field(default_factory=list)
|
|
108
|
+
insert_into_class: Optional[str] = None
|
|
109
|
+
insert_into_function: Optional[str] = None
|
|
110
|
+
method_kind: Optional[Literal["instance", "classmethod", "staticmethod"]] = None
|
|
111
|
+
method_param_name: Optional[str] = None
|
|
112
|
+
|
|
113
|
+
def __post_init__(self) -> None:
|
|
114
|
+
"""Coerce legacy tuple replacements into :class:`Replacement` instances."""
|
|
115
|
+
|
|
116
|
+
coerced: List[Replacement] = []
|
|
117
|
+
for item in self.replacements:
|
|
118
|
+
if isinstance(item, Replacement):
|
|
119
|
+
coerced.append(item)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if not isinstance(item, tuple):
|
|
123
|
+
raise TypeError(
|
|
124
|
+
"Replacement entries must be Replacement instances or tuples, "
|
|
125
|
+
f"got {type(item)!r}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if len(item) == 4:
|
|
129
|
+
line_range, node, file_path, class_name = item
|
|
130
|
+
elif len(item) == 3:
|
|
131
|
+
line_range, node, file_path = item
|
|
132
|
+
class_name = None
|
|
133
|
+
elif len(item) == 2:
|
|
134
|
+
line_range, node = item
|
|
135
|
+
file_path = None
|
|
136
|
+
class_name = None
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"Replacement tuple must have length 2, 3, or 4 ("
|
|
140
|
+
"line_range, node[, file_path[, class_name]])"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
method_kind: Optional[Literal["instance", "classmethod", "staticmethod"]] = None
|
|
144
|
+
implicit_param: Optional[str] = None
|
|
145
|
+
if class_name is not None:
|
|
146
|
+
method_kind = "instance"
|
|
147
|
+
implicit_param = "self"
|
|
148
|
+
|
|
149
|
+
coerced.append(
|
|
150
|
+
Replacement(
|
|
151
|
+
line_range=line_range,
|
|
152
|
+
node=node,
|
|
153
|
+
file_path=file_path,
|
|
154
|
+
class_name=class_name,
|
|
155
|
+
method_kind=method_kind,
|
|
156
|
+
implicit_param=implicit_param,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
self.replacements = coerced
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class ParsedModule:
|
|
165
|
+
"""Container for parsed module data flowing through the pipeline."""
|
|
166
|
+
|
|
167
|
+
file_path: str
|
|
168
|
+
source: str
|
|
169
|
+
tree: ast.AST
|
|
170
|
+
scope_analyzer: Optional["ScopeAnalyzer"] = None
|
|
171
|
+
root_scope: Optional["Scope"] = None
|
|
172
|
+
class_infos: List[ClassInfo] = field(default_factory=list)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class FunctionArtifact:
|
|
177
|
+
"""Represents a function (sync or async) discovered during analysis with context."""
|
|
178
|
+
|
|
179
|
+
file_path: str
|
|
180
|
+
node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
|
|
181
|
+
source: str
|
|
182
|
+
scope_analyzer: "ScopeAnalyzer"
|
|
183
|
+
root_scope: "Scope"
|
|
184
|
+
class_name: Optional[str]
|
|
185
|
+
enclosing_function: Optional[str]
|
|
186
|
+
ancestry: List[str]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if TYPE_CHECKING: # pragma: no cover - imported for typing only
|
|
190
|
+
from .scope_analyzer import ScopeAnalyzer, Scope
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright 2025 Eric Allen
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
Nominal Unification for Python Code Blocks
|
|
18
|
+
|
|
19
|
+
This module implements nominal unification to handle α-equivalence: code blocks
|
|
20
|
+
that differ only in the names of bound variables.
|
|
21
|
+
|
|
22
|
+
Based on the Nominal Unification algorithm:
|
|
23
|
+
M. J. Gabbay. Urban, C., Pitts, A. M., & Gabbay, M. J. (2003).
|
|
24
|
+
Nominal unification. In *Computer Science Logic* (pp. 513-527).
|
|
25
|
+
Springer, Berlin, Heidelberg.
|
|
26
|
+
Calvès, C., & Fernández, M. (2008). A polynomial nominal unification algorithm.
|
|
27
|
+
*Theoretical Computer Science*, *403*(2-3), 285-306.
|
|
28
|
+
Urban, C. (2010). Nominal Unification Revisited.
|
|
29
|
+
Electronic Proceedings in Theoretical Computer Science, 42, 1–11.
|
|
30
|
+
|
|
31
|
+
Example of α-equivalent blocks:
|
|
32
|
+
Block 1: user = get_user(); validate(user)
|
|
33
|
+
Block 2: admin = get_admin(); validate(admin)
|
|
34
|
+
|
|
35
|
+
These blocks are structurally identical - 'user' and 'admin' play the same role.
|
|
36
|
+
Nominal unification tracks this correspondence so that extracted function calls
|
|
37
|
+
use the correct variable names:
|
|
38
|
+
- Call in block 1: validate_func(user)
|
|
39
|
+
- Call in block 2: validate_func(admin) # NOT validate_func(user)!
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import ast
|
|
43
|
+
from typing import Dict, List, Set, Optional, Tuple, cast
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
|
|
46
|
+
from .binding_detector import Binding, detect_bindings
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class VariableCorrespondence:
|
|
51
|
+
"""
|
|
52
|
+
Tracks how variables correspond across unified code blocks.
|
|
53
|
+
|
|
54
|
+
For α-equivalent blocks like:
|
|
55
|
+
Block 0: user = ...; process(user)
|
|
56
|
+
Block 1: admin = ...; process(admin)
|
|
57
|
+
|
|
58
|
+
The correspondence tracks:
|
|
59
|
+
canonical_name = 'user' # We pick one as the canonical name
|
|
60
|
+
block_to_original = {0: 'user', 1: 'admin'} # Original names per block
|
|
61
|
+
original_to_canonical = {('user', 0): 'user', ('admin', 1): 'user'}
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
canonical_name: str # The canonical/template variable name
|
|
65
|
+
block_to_original: Dict[int, str] = field(default_factory=dict) # block_idx → original name
|
|
66
|
+
original_to_canonical: Dict[Tuple[str, int], str] = field(
|
|
67
|
+
default_factory=dict
|
|
68
|
+
) # (name, block_idx) → canonical
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class NominalUnificationContext:
|
|
72
|
+
"""
|
|
73
|
+
Context for nominal unification across multiple code blocks.
|
|
74
|
+
|
|
75
|
+
Tracks:
|
|
76
|
+
- Variable correspondences (which variables in different blocks correspond)
|
|
77
|
+
- Binding sites (where each variable is bound)
|
|
78
|
+
- Freshness constraints (which variables must not clash)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, num_blocks: int):
|
|
82
|
+
self.num_blocks = num_blocks
|
|
83
|
+
|
|
84
|
+
# Track variable correspondences across blocks
|
|
85
|
+
# Maps canonical variable name → VariableCorrespondence
|
|
86
|
+
self.correspondences: Dict[str, VariableCorrespondence] = {}
|
|
87
|
+
|
|
88
|
+
# Track which variables are bound in each block
|
|
89
|
+
# Maps block_idx → set of bound variable names
|
|
90
|
+
self.bound_variables: List[Set[str]] = [set() for _ in range(num_blocks)]
|
|
91
|
+
|
|
92
|
+
# Track binding sites for each variable in each block
|
|
93
|
+
# Maps (block_idx, var_name) → list of AST nodes where variable is bound
|
|
94
|
+
self.binding_sites: Dict[Tuple[int, str], List[ast.AST]] = {}
|
|
95
|
+
|
|
96
|
+
def add_correspondence(self, canonical_name: str, block_idx: int, original_name: str) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Record that 'original_name' in block 'block_idx' corresponds to 'canonical_name'.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
canonical_name: The canonical variable name (from the template)
|
|
102
|
+
block_idx: Which code block this is
|
|
103
|
+
original_name: The actual variable name in this block
|
|
104
|
+
"""
|
|
105
|
+
if canonical_name not in self.correspondences:
|
|
106
|
+
self.correspondences[canonical_name] = VariableCorrespondence(
|
|
107
|
+
canonical_name=canonical_name
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
corr = self.correspondences[canonical_name]
|
|
111
|
+
corr.block_to_original[block_idx] = original_name
|
|
112
|
+
corr.original_to_canonical[(original_name, block_idx)] = canonical_name
|
|
113
|
+
|
|
114
|
+
def get_original_name(self, canonical_name: str, block_idx: int) -> Optional[str]:
|
|
115
|
+
"""
|
|
116
|
+
Get the original variable name in a specific block for a canonical variable.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
canonical_name: The canonical variable name
|
|
120
|
+
block_idx: Which block to look up
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The original variable name in that block, or None if not found
|
|
124
|
+
"""
|
|
125
|
+
if canonical_name not in self.correspondences:
|
|
126
|
+
return None
|
|
127
|
+
return self.correspondences[canonical_name].block_to_original.get(block_idx)
|
|
128
|
+
|
|
129
|
+
def get_canonical_name(self, original_name: str, block_idx: int) -> Optional[str]:
|
|
130
|
+
"""
|
|
131
|
+
Get the canonical variable name for an original variable in a specific block.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
original_name: The variable name in the block
|
|
135
|
+
block_idx: Which block this is
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The canonical variable name, or None if not found
|
|
139
|
+
"""
|
|
140
|
+
for corr in self.correspondences.values():
|
|
141
|
+
if corr.original_to_canonical.get((original_name, block_idx)):
|
|
142
|
+
return corr.canonical_name
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
def detect_bindings_in_blocks(self, blocks: List[List[ast.AST]]) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Analyze all code blocks to detect bound variables.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
blocks: List of code blocks (each block is a list of AST nodes)
|
|
151
|
+
"""
|
|
152
|
+
for block_idx, block in enumerate(blocks):
|
|
153
|
+
# Create a module to contain the block for analysis
|
|
154
|
+
module = ast.Module(body=cast(List[ast.stmt], block), type_ignores=[])
|
|
155
|
+
|
|
156
|
+
# Detect all bindings in this block
|
|
157
|
+
bindings = detect_bindings(module)
|
|
158
|
+
|
|
159
|
+
# Record bound variables
|
|
160
|
+
for binding in bindings:
|
|
161
|
+
self.bound_variables[block_idx].add(binding.name)
|
|
162
|
+
|
|
163
|
+
# Record binding site
|
|
164
|
+
_record_binding_sites(self.binding_sites, block_idx, binding)
|
|
165
|
+
|
|
166
|
+
def is_bound_in_block(self, var_name: str, block_idx: int) -> bool:
|
|
167
|
+
"""Check if a variable is bound in a specific block."""
|
|
168
|
+
return var_name in self.bound_variables[block_idx]
|
|
169
|
+
|
|
170
|
+
def export_to_hygienic_renames(self) -> List[Dict[str, str]]:
|
|
171
|
+
"""
|
|
172
|
+
Export correspondences to the hygienic_renames format.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List of dictionaries, one per block, mapping original → canonical names
|
|
176
|
+
"""
|
|
177
|
+
hygienic_renames: List[Dict[str, str]] = [{} for _ in range(self.num_blocks)]
|
|
178
|
+
|
|
179
|
+
for corr in self.correspondences.values():
|
|
180
|
+
for block_idx, original_name in corr.block_to_original.items():
|
|
181
|
+
if original_name != corr.canonical_name:
|
|
182
|
+
hygienic_renames[block_idx][original_name] = corr.canonical_name
|
|
183
|
+
|
|
184
|
+
return hygienic_renames
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class NominalVariableMatcher:
|
|
188
|
+
"""
|
|
189
|
+
Matches variables across blocks to identify α-equivalent patterns.
|
|
190
|
+
|
|
191
|
+
This is the core of nominal unification: given two code blocks with different
|
|
192
|
+
variable names, determine which variables correspond to each other.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(self, context: NominalUnificationContext):
|
|
196
|
+
self.context = context
|
|
197
|
+
|
|
198
|
+
def try_match_variables(self, var1: str, var2: str, block_idx1: int, block_idx2: int) -> bool:
|
|
199
|
+
"""
|
|
200
|
+
Try to match two variables from different blocks.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
var1: Variable name in block 1
|
|
204
|
+
var2: Variable name in block 2
|
|
205
|
+
block_idx1: Index of first block
|
|
206
|
+
block_idx2: Index of second block
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
True if the variables can be matched (or are already matched), False otherwise
|
|
210
|
+
"""
|
|
211
|
+
# Check if both variables are bound in their respective blocks
|
|
212
|
+
is_bound1 = self.context.is_bound_in_block(var1, block_idx1)
|
|
213
|
+
is_bound2 = self.context.is_bound_in_block(var2, block_idx2)
|
|
214
|
+
|
|
215
|
+
# If binding status differs, they can't match
|
|
216
|
+
if is_bound1 != is_bound2:
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
# If neither is bound, they're free variables - must have same name
|
|
220
|
+
if not is_bound1 and not is_bound2:
|
|
221
|
+
return var1 == var2
|
|
222
|
+
|
|
223
|
+
# Both are bound - check if they're already in a correspondence
|
|
224
|
+
canonical1 = self.context.get_canonical_name(var1, block_idx1)
|
|
225
|
+
canonical2 = self.context.get_canonical_name(var2, block_idx2)
|
|
226
|
+
|
|
227
|
+
if canonical1 and canonical2:
|
|
228
|
+
# Both already have canonical names - must be the same
|
|
229
|
+
return canonical1 == canonical2
|
|
230
|
+
|
|
231
|
+
if canonical1:
|
|
232
|
+
# var1 has a canonical name, var2 doesn't - add var2 to the same correspondence
|
|
233
|
+
self.context.add_correspondence(canonical1, block_idx2, var2)
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
if canonical2:
|
|
237
|
+
# var2 has a canonical name, var1 doesn't - add var1 to the same correspondence
|
|
238
|
+
self.context.add_correspondence(canonical2, block_idx1, var1)
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# Neither has a canonical name yet - create new correspondence
|
|
242
|
+
# Use var1 as the canonical name
|
|
243
|
+
canonical_name = var1
|
|
244
|
+
self.context.add_correspondence(canonical_name, block_idx1, var1)
|
|
245
|
+
self.context.add_correspondence(canonical_name, block_idx2, var2)
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
def match_name_nodes(
|
|
249
|
+
self, node1: ast.Name, node2: ast.Name, block_idx1: int, block_idx2: int
|
|
250
|
+
) -> bool:
|
|
251
|
+
"""
|
|
252
|
+
Try to match two Name nodes from different blocks.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
node1: Name node from block 1
|
|
256
|
+
node2: Name node from block 2
|
|
257
|
+
block_idx1: Index of first block
|
|
258
|
+
block_idx2: Index of second block
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
True if the names can be matched, False otherwise
|
|
262
|
+
"""
|
|
263
|
+
return self.try_match_variables(node1.id, node2.id, block_idx1, block_idx2)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def analyze_nominal_patterns(blocks: List[List[ast.AST]]) -> NominalUnificationContext:
|
|
267
|
+
"""
|
|
268
|
+
Analyze code blocks to identify nominal (α-equivalence) patterns.
|
|
269
|
+
|
|
270
|
+
This is a simplified version that detects bound variables but doesn't
|
|
271
|
+
do full structural matching. Full structural matching will be integrated
|
|
272
|
+
into the existing Unifier class.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
blocks: List of code blocks to analyze
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Context containing variable correspondences
|
|
279
|
+
"""
|
|
280
|
+
context = NominalUnificationContext(num_blocks=len(blocks))
|
|
281
|
+
|
|
282
|
+
# Detect all bindings in all blocks
|
|
283
|
+
context.detect_bindings_in_blocks(blocks)
|
|
284
|
+
|
|
285
|
+
return context
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def build_hygienic_renames_from_unification(
|
|
289
|
+
blocks: List[List[ast.AST]], canonical_block: List[ast.AST], canonical_idx: int = 0
|
|
290
|
+
) -> List[Dict[str, str]]:
|
|
291
|
+
"""
|
|
292
|
+
Build hygienic_renames mapping by comparing blocks to a canonical template.
|
|
293
|
+
|
|
294
|
+
This function is designed to be called after traditional unification has
|
|
295
|
+
succeeded, to populate the hygienic_renames with variable correspondences.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
blocks: All unified code blocks
|
|
299
|
+
canonical_block: The canonical/template block (typically blocks[0])
|
|
300
|
+
canonical_idx: Index of the canonical block (default 0)
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
List of dicts mapping original names → canonical names for each block
|
|
304
|
+
"""
|
|
305
|
+
context = NominalUnificationContext(num_blocks=len(blocks))
|
|
306
|
+
context.detect_bindings_in_blocks(blocks)
|
|
307
|
+
|
|
308
|
+
# Get bound variables from canonical block
|
|
309
|
+
canonical_bound = context.bound_variables[canonical_idx]
|
|
310
|
+
|
|
311
|
+
# For each other block, find corresponding variables
|
|
312
|
+
for block_idx, block in enumerate(blocks):
|
|
313
|
+
if block_idx == canonical_idx:
|
|
314
|
+
# Canonical block: identity mapping
|
|
315
|
+
for var in canonical_bound:
|
|
316
|
+
context.add_correspondence(var, block_idx, var)
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# Match bound variables between canonical and this block
|
|
320
|
+
block_bound = context.bound_variables[block_idx]
|
|
321
|
+
|
|
322
|
+
# Simple heuristic: match variables in order of first occurrence
|
|
323
|
+
# This works for our use case where blocks have parallel structure
|
|
324
|
+
canonical_vars_ordered = _get_variables_in_order(canonical_block)
|
|
325
|
+
block_vars_ordered = _get_variables_in_order(block)
|
|
326
|
+
|
|
327
|
+
# Filter to bound variables only
|
|
328
|
+
canonical_bound_ordered = [v for v in canonical_vars_ordered if v in canonical_bound]
|
|
329
|
+
block_bound_ordered = [v for v in block_vars_ordered if v in block_bound]
|
|
330
|
+
|
|
331
|
+
# Match in order
|
|
332
|
+
for canonical_var, block_var in zip(canonical_bound_ordered, block_bound_ordered):
|
|
333
|
+
context.add_correspondence(canonical_var, canonical_idx, canonical_var)
|
|
334
|
+
context.add_correspondence(canonical_var, block_idx, block_var)
|
|
335
|
+
|
|
336
|
+
return context.export_to_hygienic_renames()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _get_variables_in_order(block: List[ast.AST]) -> List[str]:
|
|
340
|
+
"""
|
|
341
|
+
Get all variable names from a block in order of first occurrence.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
block: List of AST statements
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
List of variable names in order of first use
|
|
348
|
+
"""
|
|
349
|
+
seen = set()
|
|
350
|
+
ordered = []
|
|
351
|
+
|
|
352
|
+
class VariableCollector(ast.NodeVisitor):
|
|
353
|
+
def visit_Name(self, node: ast.Name) -> None:
|
|
354
|
+
if node.id not in seen:
|
|
355
|
+
seen.add(node.id)
|
|
356
|
+
ordered.append(node.id)
|
|
357
|
+
self.generic_visit(node)
|
|
358
|
+
|
|
359
|
+
collector = VariableCollector()
|
|
360
|
+
return _gather_variables_in_order(block, collector, ordered)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _record_binding_sites(
|
|
364
|
+
table: Dict[Tuple[int, str], List[ast.AST]], block_idx: int, binding: Binding
|
|
365
|
+
) -> None:
|
|
366
|
+
key = (block_idx, binding.name)
|
|
367
|
+
table.setdefault(key, []).append(binding.node)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _gather_variables_in_order(
|
|
371
|
+
block: List[ast.AST], collector: ast.NodeVisitor, ordered: List[str]
|
|
372
|
+
) -> List[str]:
|
|
373
|
+
for stmt in block:
|
|
374
|
+
collector.visit(stmt)
|
|
375
|
+
return ordered
|