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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- 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)
|
yuho/ast/reachability.py
ADDED
|
@@ -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)
|