yuho 5.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.
Files changed (91) hide show
  1. yuho/__init__.py +16 -0
  2. yuho/ast/__init__.py +196 -0
  3. yuho/ast/builder.py +926 -0
  4. yuho/ast/constant_folder.py +280 -0
  5. yuho/ast/dead_code.py +199 -0
  6. yuho/ast/exhaustiveness.py +503 -0
  7. yuho/ast/nodes.py +907 -0
  8. yuho/ast/overlap.py +291 -0
  9. yuho/ast/reachability.py +293 -0
  10. yuho/ast/scope_analysis.py +490 -0
  11. yuho/ast/transformer.py +490 -0
  12. yuho/ast/type_check.py +471 -0
  13. yuho/ast/type_inference.py +425 -0
  14. yuho/ast/visitor.py +239 -0
  15. yuho/cli/__init__.py +14 -0
  16. yuho/cli/commands/__init__.py +1 -0
  17. yuho/cli/commands/api.py +431 -0
  18. yuho/cli/commands/ast_viz.py +334 -0
  19. yuho/cli/commands/check.py +218 -0
  20. yuho/cli/commands/config.py +311 -0
  21. yuho/cli/commands/contribute.py +122 -0
  22. yuho/cli/commands/diff.py +487 -0
  23. yuho/cli/commands/explain.py +240 -0
  24. yuho/cli/commands/fmt.py +253 -0
  25. yuho/cli/commands/generate.py +316 -0
  26. yuho/cli/commands/graph.py +410 -0
  27. yuho/cli/commands/init.py +120 -0
  28. yuho/cli/commands/library.py +656 -0
  29. yuho/cli/commands/lint.py +503 -0
  30. yuho/cli/commands/lsp.py +36 -0
  31. yuho/cli/commands/preview.py +377 -0
  32. yuho/cli/commands/repl.py +444 -0
  33. yuho/cli/commands/serve.py +44 -0
  34. yuho/cli/commands/test.py +528 -0
  35. yuho/cli/commands/transpile.py +121 -0
  36. yuho/cli/commands/wizard.py +370 -0
  37. yuho/cli/completions.py +182 -0
  38. yuho/cli/error_formatter.py +193 -0
  39. yuho/cli/main.py +1064 -0
  40. yuho/config/__init__.py +46 -0
  41. yuho/config/loader.py +235 -0
  42. yuho/config/mask.py +194 -0
  43. yuho/config/schema.py +147 -0
  44. yuho/library/__init__.py +84 -0
  45. yuho/library/index.py +328 -0
  46. yuho/library/install.py +699 -0
  47. yuho/library/lockfile.py +330 -0
  48. yuho/library/package.py +421 -0
  49. yuho/library/resolver.py +791 -0
  50. yuho/library/signature.py +335 -0
  51. yuho/llm/__init__.py +45 -0
  52. yuho/llm/config.py +75 -0
  53. yuho/llm/factory.py +123 -0
  54. yuho/llm/prompts.py +146 -0
  55. yuho/llm/providers.py +383 -0
  56. yuho/llm/utils.py +470 -0
  57. yuho/lsp/__init__.py +14 -0
  58. yuho/lsp/code_action_handler.py +518 -0
  59. yuho/lsp/completion_handler.py +85 -0
  60. yuho/lsp/diagnostics.py +100 -0
  61. yuho/lsp/hover_handler.py +130 -0
  62. yuho/lsp/server.py +1425 -0
  63. yuho/mcp/__init__.py +10 -0
  64. yuho/mcp/server.py +1452 -0
  65. yuho/parser/__init__.py +8 -0
  66. yuho/parser/source_location.py +108 -0
  67. yuho/parser/wrapper.py +311 -0
  68. yuho/testing/__init__.py +48 -0
  69. yuho/testing/coverage.py +274 -0
  70. yuho/testing/fixtures.py +263 -0
  71. yuho/transpile/__init__.py +52 -0
  72. yuho/transpile/alloy_transpiler.py +546 -0
  73. yuho/transpile/base.py +100 -0
  74. yuho/transpile/blocks_transpiler.py +338 -0
  75. yuho/transpile/english_transpiler.py +470 -0
  76. yuho/transpile/graphql_transpiler.py +404 -0
  77. yuho/transpile/json_transpiler.py +217 -0
  78. yuho/transpile/jsonld_transpiler.py +250 -0
  79. yuho/transpile/latex_preamble.py +161 -0
  80. yuho/transpile/latex_transpiler.py +406 -0
  81. yuho/transpile/latex_utils.py +206 -0
  82. yuho/transpile/mermaid_transpiler.py +357 -0
  83. yuho/transpile/registry.py +275 -0
  84. yuho/verify/__init__.py +43 -0
  85. yuho/verify/alloy.py +352 -0
  86. yuho/verify/combined.py +218 -0
  87. yuho/verify/z3_solver.py +1155 -0
  88. yuho-5.0.0.dist-info/METADATA +186 -0
  89. yuho-5.0.0.dist-info/RECORD +91 -0
  90. yuho-5.0.0.dist-info/WHEEL +4 -0
  91. yuho-5.0.0.dist-info/entry_points.txt +2 -0
yuho/ast/overlap.py ADDED
@@ -0,0 +1,291 @@
1
+ """
2
+ Overlap detector for match expression patterns.
3
+
4
+ Detects when match arm patterns overlap, which may indicate legal ambiguity
5
+ in statute definitions. Two patterns overlap when they can both match
6
+ the same value, creating potential interpretation conflicts.
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import List, Optional, Set, Tuple, Any
11
+
12
+ from yuho.ast import nodes
13
+ from yuho.ast.visitor import Visitor
14
+ from yuho.ast.type_inference import TypeInferenceResult
15
+ from yuho.ast.exhaustiveness import (
16
+ AbstractPattern,
17
+ PatternKind,
18
+ PatternExtractor,
19
+ )
20
+
21
+
22
+ @dataclass
23
+ class PatternOverlap:
24
+ """Information about overlapping patterns."""
25
+
26
+ arm1_index: int
27
+ arm2_index: int
28
+ arm1_line: int = 0
29
+ arm2_line: int = 0
30
+ description: str = ""
31
+
32
+ def __str__(self) -> str:
33
+ return (
34
+ f"Arms #{self.arm1_index + 1} and #{self.arm2_index + 1} overlap: "
35
+ f"{self.description}"
36
+ )
37
+
38
+
39
+ @dataclass
40
+ class OverlapWarning:
41
+ """Warning for overlapping patterns."""
42
+
43
+ message: str
44
+ line: int = 0
45
+ column: int = 0
46
+ arm1_index: int = -1
47
+ arm2_index: int = -1
48
+ severity: str = "warning"
49
+
50
+ def __str__(self) -> str:
51
+ loc = f":{self.line}:{self.column}" if self.line else ""
52
+ return f"{loc} warning: {self.message}"
53
+
54
+
55
+ @dataclass
56
+ class OverlapResult:
57
+ """Result of overlap analysis for a match expression."""
58
+
59
+ match_node: Optional[nodes.MatchExprNode]
60
+ overlaps: List[PatternOverlap] = field(default_factory=list)
61
+
62
+ @property
63
+ def has_overlaps(self) -> bool:
64
+ return len(self.overlaps) > 0
65
+
66
+
67
+ class OverlapDetector(Visitor):
68
+ """
69
+ Detects overlapping patterns in match expressions.
70
+
71
+ Two patterns overlap if they can match the same value. This is important
72
+ in legal contexts where overlapping statute elements may create ambiguity
73
+ about which provision applies.
74
+
75
+ Note: This differs from reachability - overlapping patterns can both be
76
+ reachable if neither completely covers the other, but they share some
77
+ common cases.
78
+
79
+ Usage:
80
+ detector = OverlapDetector()
81
+ module.accept(detector)
82
+
83
+ for warning in detector.warnings:
84
+ print(warning)
85
+ """
86
+
87
+ def __init__(self, type_info: Optional[TypeInferenceResult] = None):
88
+ self.type_info = type_info or TypeInferenceResult()
89
+ self.warnings: List[OverlapWarning] = []
90
+ self.results: List[OverlapResult] = []
91
+ self._extractor = PatternExtractor(type_info)
92
+
93
+ def check(self, module: nodes.ModuleNode) -> List[OverlapWarning]:
94
+ """Check all match expressions in module for overlapping patterns."""
95
+ self.visit(module)
96
+ return self.warnings
97
+
98
+ def _patterns_overlap(
99
+ self,
100
+ p1: AbstractPattern,
101
+ p2: AbstractPattern,
102
+ ) -> bool:
103
+ """
104
+ Check if two patterns can match the same value.
105
+
106
+ Patterns overlap if there exists some value v such that
107
+ both p1 and p2 would match v.
108
+
109
+ Cases:
110
+ - Wildcard overlaps with everything
111
+ - Same literal values overlap
112
+ - Different literal values don't overlap
113
+ - Struct patterns overlap if same constructor and all fields overlap
114
+ """
115
+ # Wildcards overlap with everything
116
+ if p1.is_wildcard() or p2.is_wildcard():
117
+ return True
118
+
119
+ # Guards make overlap uncertain - assume overlap to be safe
120
+ if p1.has_guard or p2.has_guard:
121
+ return True
122
+
123
+ # Different kinds don't overlap (except with wildcards, handled above)
124
+ if p1.kind != p2.kind:
125
+ return False
126
+
127
+ if p1.kind == PatternKind.LITERAL:
128
+ # Literals overlap only if same value
129
+ return p1.value == p2.value
130
+
131
+ if p1.kind == PatternKind.STRUCT:
132
+ # Struct patterns overlap if same constructor and fields overlap
133
+ if p1.value != p2.value:
134
+ return False
135
+
136
+ # Check field patterns
137
+ if len(p1.children) != len(p2.children):
138
+ # Different arities - shouldn't happen for same constructor
139
+ return False
140
+
141
+ # All corresponding fields must overlap for structs to overlap
142
+ for c1, c2 in zip(p1.children, p2.children):
143
+ if not self._patterns_overlap(c1, c2):
144
+ return False
145
+
146
+ return True
147
+
148
+ # Unknown pattern kinds - assume overlap to be safe
149
+ return True
150
+
151
+ def _describe_overlap(
152
+ self,
153
+ p1: AbstractPattern,
154
+ p2: AbstractPattern,
155
+ ) -> str:
156
+ """Generate a description of why patterns overlap."""
157
+ if p1.is_wildcard() and p2.is_wildcard():
158
+ return "both are catch-all patterns"
159
+
160
+ if p1.is_wildcard():
161
+ return f"wildcard overlaps with {self._pattern_str(p2)}"
162
+
163
+ if p2.is_wildcard():
164
+ return f"{self._pattern_str(p1)} overlaps with wildcard"
165
+
166
+ if p1.kind == PatternKind.LITERAL and p2.kind == PatternKind.LITERAL:
167
+ if p1.value == p2.value:
168
+ return f"duplicate literal pattern: {p1.value}"
169
+
170
+ if p1.kind == PatternKind.STRUCT and p2.kind == PatternKind.STRUCT:
171
+ if p1.value == p2.value:
172
+ return f"both match constructor '{p1.value}'"
173
+
174
+ return "patterns can match the same value"
175
+
176
+ def _pattern_str(self, p: AbstractPattern) -> str:
177
+ """Convert pattern to string for display."""
178
+ if p.is_wildcard():
179
+ return "_"
180
+
181
+ if p.kind == PatternKind.LITERAL:
182
+ if isinstance(p.value, bool):
183
+ return "TRUE" if p.value else "FALSE"
184
+ if isinstance(p.value, str):
185
+ return f'"{p.value}"'
186
+ return str(p.value)
187
+
188
+ if p.kind == PatternKind.STRUCT:
189
+ children_str = ", ".join(self._pattern_str(c) for c in p.children)
190
+ if children_str:
191
+ return f"{p.value}({children_str})"
192
+ return str(p.value)
193
+
194
+ return "?"
195
+
196
+ def _get_arm_location(self, arm: nodes.MatchArm) -> Tuple[int, int]:
197
+ """Extract source location from match arm."""
198
+ if arm.source_location:
199
+ return arm.source_location.start_line, arm.source_location.start_column
200
+ if arm.pattern.source_location:
201
+ return arm.pattern.source_location.start_line, arm.pattern.source_location.start_column
202
+ return 0, 0
203
+
204
+ def _check_match_overlaps(
205
+ self,
206
+ node: nodes.MatchExprNode,
207
+ ) -> OverlapResult:
208
+ """Check for overlapping patterns in a match expression."""
209
+ overlaps: List[PatternOverlap] = []
210
+
211
+ # Extract all patterns
212
+ patterns = []
213
+ for arm in node.arms:
214
+ pattern = self._extractor.extract(arm.pattern, has_guard=arm.guard is not None)
215
+ patterns.append(pattern)
216
+
217
+ # Compare each pair of patterns
218
+ for i in range(len(patterns)):
219
+ for j in range(i + 1, len(patterns)):
220
+ p1, p2 = patterns[i], patterns[j]
221
+
222
+ # Skip if one completely covers the other (handled by reachability)
223
+ # We only care about partial overlaps
224
+ if p1.covers(p2) or p2.covers(p1):
225
+ continue
226
+
227
+ if self._patterns_overlap(p1, p2):
228
+ line1, col1 = self._get_arm_location(node.arms[i])
229
+ line2, col2 = self._get_arm_location(node.arms[j])
230
+
231
+ overlaps.append(PatternOverlap(
232
+ arm1_index=i,
233
+ arm2_index=j,
234
+ arm1_line=line1,
235
+ arm2_line=line2,
236
+ description=self._describe_overlap(p1, p2),
237
+ ))
238
+
239
+ return OverlapResult(
240
+ match_node=node,
241
+ overlaps=overlaps,
242
+ )
243
+
244
+ def visit_match_expr(self, node: nodes.MatchExprNode) -> Any:
245
+ """Check for overlapping patterns in match expression."""
246
+ # Visit children first
247
+ self.generic_visit(node)
248
+
249
+ result = self._check_match_overlaps(node)
250
+ self.results.append(result)
251
+
252
+ # Generate warnings for overlaps
253
+ for overlap in result.overlaps:
254
+ # Use location of first arm
255
+ line = overlap.arm1_line
256
+ column = 0
257
+
258
+ self.warnings.append(OverlapWarning(
259
+ message=(
260
+ f"Match arms #{overlap.arm1_index + 1} and #{overlap.arm2_index + 1} "
261
+ f"overlap: {overlap.description}"
262
+ ),
263
+ line=line,
264
+ column=column,
265
+ arm1_index=overlap.arm1_index,
266
+ arm2_index=overlap.arm2_index,
267
+ ))
268
+
269
+ return None
270
+
271
+ def visit_module(self, node: nodes.ModuleNode) -> Any:
272
+ """Entry point: check all match expressions in module."""
273
+ return self.generic_visit(node)
274
+
275
+
276
+ def check_overlaps(
277
+ module: nodes.ModuleNode,
278
+ type_info: Optional[TypeInferenceResult] = None,
279
+ ) -> List[OverlapWarning]:
280
+ """
281
+ Check for overlapping patterns in all match expressions.
282
+
283
+ Args:
284
+ module: The module AST to check
285
+ type_info: Optional type inference result for better analysis
286
+
287
+ Returns:
288
+ List of warnings for overlapping patterns
289
+ """
290
+ detector = OverlapDetector(type_info)
291
+ return detector.check(module)
@@ -0,0 +1,293 @@
1
+ """
2
+ Reachability checker for match expressions.
3
+
4
+ Detects unreachable match arms (dead code) by analyzing whether
5
+ earlier patterns already cover all cases that a later pattern would match.
6
+ Uses the same pattern matrix techniques as exhaustiveness checking.
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import List, Optional, Set, Dict, Any
11
+
12
+ from yuho.ast import nodes
13
+ from yuho.ast.visitor import Visitor
14
+ from yuho.ast.type_inference import TypeInferenceResult, UNKNOWN_TYPE
15
+ from yuho.ast.exhaustiveness import (
16
+ AbstractPattern,
17
+ PatternKind,
18
+ PatternMatrix,
19
+ PatternRow,
20
+ PatternExtractor,
21
+ )
22
+
23
+
24
+ @dataclass
25
+ class UnreachableArm:
26
+ """Information about an unreachable match arm."""
27
+
28
+ arm_index: int
29
+ line: int = 0
30
+ column: int = 0
31
+ reason: str = "arm is unreachable"
32
+
33
+ def __str__(self) -> str:
34
+ loc = f":{self.line}:{self.column}" if self.line else ""
35
+ return f"{loc} {self.reason}"
36
+
37
+
38
+ @dataclass
39
+ class ReachabilityError:
40
+ """Error for unreachable code detection."""
41
+
42
+ message: str
43
+ line: int = 0
44
+ column: int = 0
45
+ arm_index: int = -1
46
+ severity: str = "warning" # Unreachable code is typically a warning
47
+
48
+ def __str__(self) -> str:
49
+ loc = f":{self.line}:{self.column}" if self.line else ""
50
+ return f"{loc} warning: {self.message}"
51
+
52
+
53
+ @dataclass
54
+ class ReachabilityResult:
55
+ """Result of reachability analysis for a match expression."""
56
+
57
+ match_node: Optional[nodes.MatchExprNode]
58
+ unreachable_arms: List[UnreachableArm] = field(default_factory=list)
59
+
60
+ @property
61
+ def all_reachable(self) -> bool:
62
+ return len(self.unreachable_arms) == 0
63
+
64
+
65
+ class ReachabilityChecker(Visitor):
66
+ """
67
+ Checks for unreachable match arms (dead code).
68
+
69
+ A match arm is unreachable if all patterns it could match are already
70
+ covered by earlier arms. This is determined by checking if adding the
71
+ arm's pattern to the matrix of previous arms would be "useless".
72
+
73
+ Usage:
74
+ type_visitor = TypeInferenceVisitor()
75
+ module.accept(type_visitor)
76
+
77
+ checker = ReachabilityChecker(type_visitor.result)
78
+ module.accept(checker)
79
+
80
+ for error in checker.errors:
81
+ print(error)
82
+ """
83
+
84
+ def __init__(self, type_info: Optional[TypeInferenceResult] = None):
85
+ self.type_info = type_info or TypeInferenceResult()
86
+ self.errors: List[ReachabilityError] = []
87
+ self.results: List[ReachabilityResult] = []
88
+ self._extractor = PatternExtractor(type_info)
89
+
90
+ def check(self, module: nodes.ModuleNode) -> List[ReachabilityError]:
91
+ """Check all match expressions in module for unreachable arms."""
92
+ self.visit(module)
93
+ return self.errors
94
+
95
+ def _is_pattern_useful(
96
+ self,
97
+ pattern: AbstractPattern,
98
+ preceding_patterns: List[AbstractPattern],
99
+ ) -> bool:
100
+ """
101
+ Check if a pattern is useful given preceding patterns.
102
+
103
+ A pattern is useful if it can match some value that is not
104
+ already matched by any of the preceding patterns.
105
+
106
+ Uses the pattern matrix algorithm: a pattern P is useful w.r.t
107
+ matrix M if there exists some value v such that P matches v
108
+ and no row in M matches v.
109
+ """
110
+ if not preceding_patterns:
111
+ # First pattern is always useful
112
+ return True
113
+
114
+ # If any preceding pattern is a pure wildcard, this pattern is useless
115
+ for prev in preceding_patterns:
116
+ if prev.is_wildcard():
117
+ return False
118
+
119
+ # Build matrix from preceding patterns
120
+ rows = [PatternRow([p], i) for i, p in enumerate(preceding_patterns)]
121
+ matrix = PatternMatrix(rows)
122
+
123
+ # Check usefulness of new pattern
124
+ return self._pattern_useful_in_matrix(pattern, matrix)
125
+
126
+ def _pattern_useful_in_matrix(
127
+ self,
128
+ pattern: AbstractPattern,
129
+ matrix: PatternMatrix,
130
+ ) -> bool:
131
+ """
132
+ Check if pattern is useful against the given matrix.
133
+
134
+ Algorithm:
135
+ 1. If matrix is empty, pattern is useful
136
+ 2. If pattern is wildcard and matrix has wildcard rows, not useful
137
+ 3. Otherwise, specialize by constructors
138
+ """
139
+ # Empty matrix - pattern is useful
140
+ if matrix.is_empty():
141
+ return True
142
+
143
+ # Check if matrix has any rows
144
+ if not matrix.rows or not matrix.rows[0].patterns:
145
+ return True
146
+
147
+ col = 0
148
+
149
+ # If pattern is wildcard, check if all constructors are covered
150
+ if pattern.is_wildcard():
151
+ # Pattern can match anything not already matched
152
+ # Check if there are any wildcards in the matrix at this column
153
+ for row in matrix.rows:
154
+ if col < len(row.patterns) and row.patterns[col].is_wildcard():
155
+ # Wildcard already covers everything
156
+ return False
157
+ # No wildcard in matrix means this pattern can match something new
158
+ return True
159
+
160
+ # Non-wildcard pattern: specialize matrix by this constructor
161
+ specialized = matrix.specialize(col, pattern)
162
+
163
+ # If specialized matrix is empty, pattern matches something new
164
+ if specialized.is_empty():
165
+ return True
166
+
167
+ # Check children recursively
168
+ if pattern.children:
169
+ child_patterns = list(pattern.children)
170
+ child_matrix = specialized
171
+
172
+ for i, child in enumerate(child_patterns):
173
+ if not child_matrix.rows:
174
+ return True
175
+ if not self._child_useful(child, child_matrix, i):
176
+ return False
177
+ return True
178
+
179
+ # No children, check if any row completely covers this
180
+ for row in specialized.rows:
181
+ if not row.patterns:
182
+ # Row with no remaining patterns = complete match
183
+ return False
184
+
185
+ return True
186
+
187
+ def _child_useful(
188
+ self,
189
+ child: AbstractPattern,
190
+ matrix: PatternMatrix,
191
+ col: int,
192
+ ) -> bool:
193
+ """Check if a child pattern adds useful discrimination."""
194
+ # Extract column patterns from matrix
195
+ col_patterns = []
196
+ for row in matrix.rows:
197
+ if col < len(row.patterns):
198
+ col_patterns.append(row.patterns[col])
199
+
200
+ if not col_patterns:
201
+ return True
202
+
203
+ # Check if any pattern fully covers the child
204
+ for p in col_patterns:
205
+ if p.is_wildcard():
206
+ return not child.is_wildcard()
207
+ if p.covers(child):
208
+ return False
209
+
210
+ return True
211
+
212
+ def _check_match_reachability(
213
+ self,
214
+ node: nodes.MatchExprNode,
215
+ ) -> ReachabilityResult:
216
+ """Check reachability of all arms in a match expression."""
217
+ unreachable: List[UnreachableArm] = []
218
+ preceding_patterns: List[AbstractPattern] = []
219
+
220
+ for i, arm in enumerate(node.arms):
221
+ pattern = self._extractor.extract(arm.pattern, has_guard=arm.guard is not None)
222
+
223
+ # Check if this arm is reachable
224
+ is_useful = self._is_pattern_useful(pattern, preceding_patterns)
225
+
226
+ if not is_useful:
227
+ # Extract source location
228
+ line = 0
229
+ column = 0
230
+ if arm.source_location:
231
+ line = arm.source_location.start_line
232
+ column = arm.source_location.start_column
233
+ elif arm.pattern.source_location:
234
+ line = arm.pattern.source_location.start_line
235
+ column = arm.pattern.source_location.start_column
236
+
237
+ unreachable.append(UnreachableArm(
238
+ arm_index=i,
239
+ line=line,
240
+ column=column,
241
+ reason=f"match arm #{i + 1} is unreachable (covered by earlier patterns)",
242
+ ))
243
+
244
+ # Add to preceding patterns only if no guard
245
+ # (guards make coverage conditional)
246
+ if arm.guard is None:
247
+ preceding_patterns.append(pattern)
248
+
249
+ return ReachabilityResult(
250
+ match_node=node,
251
+ unreachable_arms=unreachable,
252
+ )
253
+
254
+ def visit_match_expr(self, node: nodes.MatchExprNode) -> Any:
255
+ """Check reachability of match expression arms."""
256
+ # Visit children first
257
+ self.generic_visit(node)
258
+
259
+ result = self._check_match_reachability(node)
260
+ self.results.append(result)
261
+
262
+ # Add errors for unreachable arms
263
+ for arm in result.unreachable_arms:
264
+ self.errors.append(ReachabilityError(
265
+ message=arm.reason,
266
+ line=arm.line,
267
+ column=arm.column,
268
+ arm_index=arm.arm_index,
269
+ ))
270
+
271
+ return None
272
+
273
+ def visit_module(self, node: nodes.ModuleNode) -> Any:
274
+ """Entry point: check all match expressions in module."""
275
+ return self.generic_visit(node)
276
+
277
+
278
+ def check_reachability(
279
+ module: nodes.ModuleNode,
280
+ type_info: Optional[TypeInferenceResult] = None,
281
+ ) -> List[ReachabilityError]:
282
+ """
283
+ Check for unreachable match arms in all match expressions.
284
+
285
+ Args:
286
+ module: The module AST to check
287
+ type_info: Optional type inference result for better analysis
288
+
289
+ Returns:
290
+ List of warnings for unreachable match arms
291
+ """
292
+ checker = ReachabilityChecker(type_info)
293
+ return checker.check(module)