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
@@ -0,0 +1,503 @@
1
+ """
2
+ Exhaustiveness checker for match expressions using pattern matrix algorithm.
3
+
4
+ Validates that match expressions cover all possible cases based on the
5
+ scrutinee type. Uses the pattern matrix algorithm as described in
6
+ "Warnings for pattern matching" (Maranget, 2007).
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum, auto
11
+ from typing import List, Optional, Set, Dict, Tuple, Any, FrozenSet
12
+
13
+ from yuho.ast import nodes
14
+ from yuho.ast.visitor import Visitor
15
+ from yuho.ast.type_inference import (
16
+ TypeAnnotation,
17
+ TypeInferenceResult,
18
+ BOOL_TYPE,
19
+ INT_TYPE,
20
+ STRING_TYPE,
21
+ UNKNOWN_TYPE,
22
+ )
23
+
24
+
25
+ class PatternKind(Enum):
26
+ """Classification of pattern types for exhaustiveness analysis."""
27
+
28
+ WILDCARD = auto() # _ or binding pattern
29
+ LITERAL = auto() # Specific value (bool, int, string)
30
+ STRUCT = auto() # Struct/enum destructuring
31
+ GUARD = auto() # Conditional pattern (treated as partial)
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class AbstractPattern:
36
+ """
37
+ Abstract representation of a pattern for exhaustiveness analysis.
38
+
39
+ Patterns are simplified to focus on exhaustiveness:
40
+ - Wildcards and bindings are equivalent (match anything)
41
+ - Literals must be tracked for finite types (bool, enum)
42
+ - Struct patterns track constructor and field patterns
43
+ """
44
+
45
+ kind: PatternKind
46
+ value: Any = None # Literal value or struct name
47
+ children: Tuple["AbstractPattern", ...] = ()
48
+ has_guard: bool = False
49
+
50
+ def is_wildcard(self) -> bool:
51
+ """True if this pattern matches all values."""
52
+ return self.kind == PatternKind.WILDCARD and not self.has_guard
53
+
54
+ def covers(self, other: "AbstractPattern") -> bool:
55
+ """Check if this pattern covers another pattern."""
56
+ if self.is_wildcard():
57
+ return True
58
+ if other.is_wildcard():
59
+ return False
60
+ if self.kind != other.kind:
61
+ return False
62
+ if self.value != other.value:
63
+ return False
64
+ if len(self.children) != len(other.children):
65
+ return False
66
+ return all(c1.covers(c2) for c1, c2 in zip(self.children, other.children))
67
+
68
+
69
+ @dataclass
70
+ class PatternRow:
71
+ """Row in the pattern matrix representing one match arm."""
72
+
73
+ patterns: List[AbstractPattern]
74
+ arm_index: int
75
+
76
+ def is_empty(self) -> bool:
77
+ return len(self.patterns) == 0
78
+
79
+
80
+ @dataclass
81
+ class PatternMatrix:
82
+ """
83
+ Matrix of patterns for exhaustiveness checking.
84
+
85
+ Rows represent match arms, columns represent pattern positions.
86
+ The algorithm works by specialization: selecting a column and
87
+ splitting the matrix based on constructors that appear there.
88
+ """
89
+
90
+ rows: List[PatternRow]
91
+ column_types: List[TypeAnnotation] = field(default_factory=list)
92
+
93
+ def is_empty(self) -> bool:
94
+ """Matrix is empty if it has no rows."""
95
+ return len(self.rows) == 0
96
+
97
+ def has_empty_row(self) -> bool:
98
+ """Check if matrix has a row with no patterns (matching all)."""
99
+ return any(row.is_empty() for row in self.rows)
100
+
101
+ def width(self) -> int:
102
+ """Number of columns in the matrix."""
103
+ return self.rows[0].patterns if self.rows else 0
104
+
105
+ def specialize(self, col: int, constructor: AbstractPattern) -> "PatternMatrix":
106
+ """
107
+ Specialize matrix by a constructor in the given column.
108
+
109
+ For each row:
110
+ - If pattern at col is wildcard: expand to constructor's arity
111
+ - If pattern at col matches constructor: include with children expanded
112
+ - Otherwise: exclude row
113
+ """
114
+ new_rows = []
115
+
116
+ for row in self.rows:
117
+ if col >= len(row.patterns):
118
+ continue
119
+
120
+ pattern = row.patterns[col]
121
+
122
+ if pattern.is_wildcard():
123
+ # Wildcard matches constructor; expand to wildcard children
124
+ new_patterns = (
125
+ row.patterns[:col] +
126
+ [AbstractPattern(PatternKind.WILDCARD) for _ in constructor.children] +
127
+ row.patterns[col + 1:]
128
+ )
129
+ new_rows.append(PatternRow(new_patterns, row.arm_index))
130
+
131
+ elif pattern.kind == constructor.kind and pattern.value == constructor.value:
132
+ # Same constructor; expand children
133
+ new_patterns = (
134
+ row.patterns[:col] +
135
+ list(pattern.children) +
136
+ row.patterns[col + 1:]
137
+ )
138
+ new_rows.append(PatternRow(new_patterns, row.arm_index))
139
+
140
+ return PatternMatrix(new_rows, self.column_types)
141
+
142
+ def default_matrix(self, col: int) -> "PatternMatrix":
143
+ """
144
+ Create default matrix for patterns that don't match given constructors.
145
+
146
+ Keeps only rows with wildcards at the given column.
147
+ """
148
+ new_rows = []
149
+
150
+ for row in self.rows:
151
+ if col >= len(row.patterns):
152
+ continue
153
+
154
+ pattern = row.patterns[col]
155
+
156
+ if pattern.is_wildcard():
157
+ new_patterns = row.patterns[:col] + row.patterns[col + 1:]
158
+ new_rows.append(PatternRow(new_patterns, row.arm_index))
159
+
160
+ return PatternMatrix(new_rows, self.column_types)
161
+
162
+
163
+ @dataclass
164
+ class ExhaustivenessResult:
165
+ """Result of exhaustiveness checking."""
166
+
167
+ is_exhaustive: bool
168
+ missing_patterns: List[str] = field(default_factory=list)
169
+ match_node: Optional[nodes.MatchExprNode] = None
170
+
171
+ def __str__(self) -> str:
172
+ if self.is_exhaustive:
173
+ return "Match is exhaustive"
174
+ patterns = ", ".join(self.missing_patterns) if self.missing_patterns else "unknown"
175
+ return f"Non-exhaustive match: missing {patterns}"
176
+
177
+
178
+ @dataclass
179
+ class ExhaustivenessError:
180
+ """Error information for non-exhaustive match."""
181
+
182
+ message: str
183
+ line: int = 0
184
+ column: int = 0
185
+ missing_patterns: List[str] = field(default_factory=list)
186
+
187
+ def __str__(self) -> str:
188
+ loc = f":{self.line}:{self.column}" if self.line else ""
189
+ return f"{loc} {self.message}"
190
+
191
+
192
+ class PatternExtractor(Visitor):
193
+ """Extracts AbstractPattern from AST PatternNode."""
194
+
195
+ def __init__(self, type_info: Optional[TypeInferenceResult] = None):
196
+ self.type_info = type_info
197
+
198
+ def extract(self, pattern: nodes.PatternNode, has_guard: bool = False) -> AbstractPattern:
199
+ """Extract abstract pattern from AST pattern node."""
200
+ result = self.visit(pattern)
201
+ if has_guard and result.kind != PatternKind.GUARD:
202
+ # Wrap in guard pattern to indicate partial coverage
203
+ return AbstractPattern(PatternKind.GUARD, has_guard=True)
204
+ return result
205
+
206
+ def visit_wildcard_pattern(self, node: nodes.WildcardPattern) -> AbstractPattern:
207
+ return AbstractPattern(PatternKind.WILDCARD)
208
+
209
+ def visit_binding_pattern(self, node: nodes.BindingPattern) -> AbstractPattern:
210
+ # Binding patterns are wildcards from exhaustiveness perspective
211
+ return AbstractPattern(PatternKind.WILDCARD)
212
+
213
+ def visit_literal_pattern(self, node: nodes.LiteralPattern) -> AbstractPattern:
214
+ literal = node.literal
215
+
216
+ if isinstance(literal, nodes.BoolLit):
217
+ return AbstractPattern(PatternKind.LITERAL, value=literal.value)
218
+ elif isinstance(literal, nodes.IntLit):
219
+ return AbstractPattern(PatternKind.LITERAL, value=literal.value)
220
+ elif isinstance(literal, nodes.StringLit):
221
+ return AbstractPattern(PatternKind.LITERAL, value=literal.value)
222
+ else:
223
+ # Other literals treated as specific values
224
+ return AbstractPattern(PatternKind.LITERAL, value=str(literal))
225
+
226
+ def visit_struct_pattern(self, node: nodes.StructPattern) -> AbstractPattern:
227
+ children = tuple(
228
+ self.visit(fp.pattern) if fp.pattern else AbstractPattern(PatternKind.WILDCARD)
229
+ for fp in node.fields
230
+ )
231
+ return AbstractPattern(PatternKind.STRUCT, value=node.type_name, children=children)
232
+
233
+ def visit_field_pattern(self, node: nodes.FieldPattern) -> AbstractPattern:
234
+ if node.pattern:
235
+ return self.visit(node.pattern)
236
+ return AbstractPattern(PatternKind.WILDCARD)
237
+
238
+ def generic_visit(self, node: nodes.ASTNode) -> AbstractPattern:
239
+ # Unknown pattern types are treated as wildcards
240
+ return AbstractPattern(PatternKind.WILDCARD)
241
+
242
+
243
+ class ExhaustivenessChecker(Visitor):
244
+ """
245
+ Checks exhaustiveness of match expressions using pattern matrix algorithm.
246
+
247
+ For each match expression with ensure_exhaustiveness=True, verifies that
248
+ all possible values of the scrutinee type are covered by at least one arm.
249
+
250
+ Usage:
251
+ type_visitor = TypeInferenceVisitor()
252
+ module.accept(type_visitor)
253
+
254
+ checker = ExhaustivenessChecker(type_visitor.result)
255
+ module.accept(checker)
256
+
257
+ for error in checker.errors:
258
+ print(error)
259
+ """
260
+
261
+ def __init__(self, type_info: Optional[TypeInferenceResult] = None):
262
+ self.type_info = type_info or TypeInferenceResult()
263
+ self.errors: List[ExhaustivenessError] = []
264
+ self.results: List[ExhaustivenessResult] = []
265
+ self._extractor = PatternExtractor(type_info)
266
+
267
+ # Known enum/sum types and their constructors
268
+ self._enum_constructors: Dict[str, List[str]] = {}
269
+
270
+ def check(self, module: nodes.ModuleNode) -> List[ExhaustivenessError]:
271
+ """Check all match expressions in module."""
272
+ # First pass: collect enum/struct definitions
273
+ self._collect_type_info(module)
274
+
275
+ # Second pass: check match expressions
276
+ self.visit(module)
277
+
278
+ return self.errors
279
+
280
+ def _collect_type_info(self, module: nodes.ModuleNode) -> None:
281
+ """Collect enum and struct definitions for constructor analysis."""
282
+ for decl in module.declarations:
283
+ if isinstance(decl, nodes.StructDefNode):
284
+ # Check if this is an enum (fields without types are variants)
285
+ variants = []
286
+ for field_def in decl.fields:
287
+ if field_def.type_annotation is None:
288
+ # This is an enum variant
289
+ variants.append(field_def.name)
290
+
291
+ if variants:
292
+ self._enum_constructors[decl.name] = variants
293
+
294
+ def _get_type_constructors(self, type_ann: TypeAnnotation) -> List[AbstractPattern]:
295
+ """
296
+ Get all constructors for a type.
297
+
298
+ For finite types (bool, enum), returns all possible values.
299
+ For infinite types (int, string), returns empty list (use default).
300
+ """
301
+ type_name = type_ann.type_name
302
+
303
+ if type_name == "bool":
304
+ return [
305
+ AbstractPattern(PatternKind.LITERAL, value=True),
306
+ AbstractPattern(PatternKind.LITERAL, value=False),
307
+ ]
308
+
309
+ if type_name in self._enum_constructors:
310
+ return [
311
+ AbstractPattern(PatternKind.STRUCT, value=variant)
312
+ for variant in self._enum_constructors[type_name]
313
+ ]
314
+
315
+ # Infinite types - no enumerable constructors
316
+ return []
317
+
318
+ def _check_usefulness(self, matrix: PatternMatrix, scrutinee_type: TypeAnnotation) -> Optional[List[str]]:
319
+ """
320
+ Check if a pattern would be useful (not covered by existing patterns).
321
+
322
+ Returns None if matrix covers all cases (exhaustive).
323
+ Returns list of missing patterns otherwise.
324
+ """
325
+ # Base case: empty matrix means pattern is useful (not covered)
326
+ if matrix.is_empty():
327
+ return ["_"] # Wildcard represents uncovered case
328
+
329
+ # Base case: row with no patterns means all inputs matched
330
+ if matrix.has_empty_row():
331
+ return None # Exhaustive
332
+
333
+ # No patterns to check
334
+ if not matrix.rows or not matrix.rows[0].patterns:
335
+ return None
336
+
337
+ # Select first column for analysis
338
+ col = 0
339
+ constructors = self._get_column_constructors(matrix, col, scrutinee_type)
340
+
341
+ if not constructors:
342
+ # Infinite type - check default matrix
343
+ default = matrix.default_matrix(col)
344
+ return self._check_usefulness(default, UNKNOWN_TYPE)
345
+
346
+ # Check if all constructors are covered
347
+ missing = []
348
+ all_constructors_present = self._all_constructors_present(matrix, col, constructors)
349
+
350
+ for ctor in constructors:
351
+ specialized = matrix.specialize(col, ctor)
352
+
353
+ # Recursively check specialized matrix
354
+ # For simplicity, treat struct children as unknown type
355
+ child_type = UNKNOWN_TYPE
356
+ sub_result = self._check_usefulness(specialized, child_type)
357
+
358
+ if sub_result is not None:
359
+ # This constructor has uncovered cases
360
+ if ctor.kind == PatternKind.LITERAL:
361
+ missing.append(str(ctor.value))
362
+ elif ctor.kind == PatternKind.STRUCT:
363
+ missing.append(str(ctor.value))
364
+ else:
365
+ missing.extend(sub_result)
366
+
367
+ # If not all constructors present, check default matrix
368
+ if not all_constructors_present:
369
+ default = matrix.default_matrix(col)
370
+ if not default.is_empty():
371
+ default_result = self._check_usefulness(default, UNKNOWN_TYPE)
372
+ if default_result is not None:
373
+ missing.extend(default_result)
374
+ else:
375
+ # Default cases not covered
376
+ missing.append("_")
377
+
378
+ return missing if missing else None
379
+
380
+ def _get_column_constructors(
381
+ self,
382
+ matrix: PatternMatrix,
383
+ col: int,
384
+ scrutinee_type: TypeAnnotation
385
+ ) -> List[AbstractPattern]:
386
+ """Get all constructors used in a column + type-defined constructors."""
387
+ seen: Set[Tuple[PatternKind, Any]] = set()
388
+ result = []
389
+
390
+ # First add type-defined constructors
391
+ type_ctors = self._get_type_constructors(scrutinee_type)
392
+ for ctor in type_ctors:
393
+ key = (ctor.kind, ctor.value)
394
+ if key not in seen:
395
+ seen.add(key)
396
+ result.append(ctor)
397
+
398
+ # Then add constructors from patterns (for non-finite types)
399
+ for row in matrix.rows:
400
+ if col < len(row.patterns):
401
+ pattern = row.patterns[col]
402
+ if not pattern.is_wildcard() and pattern.kind != PatternKind.GUARD:
403
+ key = (pattern.kind, pattern.value)
404
+ if key not in seen:
405
+ seen.add(key)
406
+ result.append(pattern)
407
+
408
+ return result
409
+
410
+ def _all_constructors_present(
411
+ self,
412
+ matrix: PatternMatrix,
413
+ col: int,
414
+ constructors: List[AbstractPattern]
415
+ ) -> bool:
416
+ """Check if all type constructors appear in the column."""
417
+ if not constructors:
418
+ return False
419
+
420
+ present: Set[Tuple[PatternKind, Any]] = set()
421
+
422
+ for row in matrix.rows:
423
+ if col < len(row.patterns):
424
+ pattern = row.patterns[col]
425
+ if not pattern.is_wildcard():
426
+ present.add((pattern.kind, pattern.value))
427
+
428
+ required = {(c.kind, c.value) for c in constructors}
429
+ return required <= present
430
+
431
+ def visit_match_expr(self, node: nodes.MatchExprNode) -> Any:
432
+ """Check exhaustiveness of match expression."""
433
+ # Visit children first
434
+ self.generic_visit(node)
435
+
436
+ # Skip if exhaustiveness check is disabled
437
+ if not node.ensure_exhaustiveness:
438
+ return
439
+
440
+ # Build pattern matrix from arms
441
+ rows = []
442
+ for i, arm in enumerate(node.arms):
443
+ pattern = self._extractor.extract(arm.pattern, has_guard=arm.guard is not None)
444
+ rows.append(PatternRow([pattern], i))
445
+
446
+ matrix = PatternMatrix(rows)
447
+
448
+ # Get scrutinee type
449
+ scrutinee_type = UNKNOWN_TYPE
450
+ if node.scrutinee and self.type_info:
451
+ scrutinee_type = self.type_info.get_type(node.scrutinee)
452
+
453
+ # Check exhaustiveness
454
+ missing = self._check_usefulness(matrix, scrutinee_type)
455
+
456
+ result = ExhaustivenessResult(
457
+ is_exhaustive=(missing is None),
458
+ missing_patterns=missing or [],
459
+ match_node=node,
460
+ )
461
+ self.results.append(result)
462
+
463
+ if not result.is_exhaustive:
464
+ # Extract source location
465
+ line = 0
466
+ column = 0
467
+ if node.source_location:
468
+ line = node.source_location.start_line
469
+ column = node.source_location.start_column
470
+
471
+ patterns_str = ", ".join(result.missing_patterns[:5])
472
+ if len(result.missing_patterns) > 5:
473
+ patterns_str += ", ..."
474
+
475
+ self.errors.append(ExhaustivenessError(
476
+ message=f"Non-exhaustive match: patterns not covered: {patterns_str}",
477
+ line=line,
478
+ column=column,
479
+ missing_patterns=result.missing_patterns,
480
+ ))
481
+
482
+ def visit_module(self, node: nodes.ModuleNode) -> Any:
483
+ """Entry point: check all match expressions in module."""
484
+ self._collect_type_info(node)
485
+ return self.generic_visit(node)
486
+
487
+
488
+ def check_exhaustiveness(
489
+ module: nodes.ModuleNode,
490
+ type_info: Optional[TypeInferenceResult] = None
491
+ ) -> List[ExhaustivenessError]:
492
+ """
493
+ Check exhaustiveness of all match expressions in a module.
494
+
495
+ Args:
496
+ module: The module AST to check
497
+ type_info: Optional type inference result for better analysis
498
+
499
+ Returns:
500
+ List of errors for non-exhaustive match expressions
501
+ """
502
+ checker = ExhaustivenessChecker(type_info)
503
+ return checker.check(module)