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,280 @@
1
+ """
2
+ Constant folding optimization for Yuho AST.
3
+
4
+ Evaluates compile-time constant expressions and replaces them with
5
+ their literal values. This includes:
6
+ - Arithmetic operations on numeric literals (+, -, *, /)
7
+ - Boolean operations on boolean literals (&&, ||, !)
8
+ - Comparison operations on numeric literals (==, !=, <, >, <=, >=)
9
+ - String concatenation on string literals (+)
10
+ - Negation of numeric literals (-)
11
+ """
12
+
13
+ from typing import Optional, Union
14
+
15
+ from yuho.ast import nodes
16
+ from yuho.ast.transformer import Transformer
17
+
18
+
19
+ class ConstantFoldingError(Exception):
20
+ """Error during constant folding (e.g., division by zero)."""
21
+ pass
22
+
23
+
24
+ class ConstantFolder(Transformer):
25
+ """
26
+ AST transformer that evaluates compile-time constant expressions.
27
+
28
+ Traverses the AST and replaces constant expressions with their
29
+ computed literal values. Preserves source location information
30
+ for error reporting.
31
+
32
+ Usage:
33
+ folder = ConstantFolder()
34
+ optimized_ast = folder.transform(ast)
35
+
36
+ Handles:
37
+ - Integer arithmetic: 1 + 2 -> 3
38
+ - Float arithmetic: 1.5 * 2.0 -> 3.0
39
+ - Boolean logic: TRUE && FALSE -> FALSE
40
+ - Comparisons: 5 > 3 -> TRUE
41
+ - String concatenation: "a" + "b" -> "ab"
42
+ - Unary negation: -5 -> IntLit(-5)
43
+ - Unary not: !TRUE -> FALSE
44
+ """
45
+
46
+ def __init__(self, *, fold_division_by_zero: bool = False):
47
+ """
48
+ Initialize the constant folder.
49
+
50
+ Args:
51
+ fold_division_by_zero: If True, division by zero will raise
52
+ ConstantFoldingError. If False (default), the expression
53
+ is left unchanged for runtime error handling.
54
+ """
55
+ self.fold_division_by_zero = fold_division_by_zero
56
+
57
+ def transform_binary_expr(self, node: nodes.BinaryExprNode) -> nodes.ASTNode:
58
+ """
59
+ Transform binary expressions, folding constants where possible.
60
+
61
+ First transforms children (bottom-up folding), then attempts
62
+ to evaluate if both operands are literals.
63
+ """
64
+ # First, transform children to fold nested expressions
65
+ node = super().transform_binary_expr(node)
66
+
67
+ # Check if both operands are now literals
68
+ left = node.left
69
+ right = node.right
70
+ op = node.operator
71
+
72
+ # Integer operations
73
+ if isinstance(left, nodes.IntLit) and isinstance(right, nodes.IntLit):
74
+ result = self._fold_int_binary(left.value, op, right.value, node)
75
+ if result is not None:
76
+ return result
77
+
78
+ # Float operations (including mixed int/float)
79
+ if self._is_numeric_literal(left) and self._is_numeric_literal(right):
80
+ left_val = self._get_numeric_value(left)
81
+ right_val = self._get_numeric_value(right)
82
+ result = self._fold_float_binary(left_val, op, right_val, node)
83
+ if result is not None:
84
+ return result
85
+
86
+ # Boolean operations
87
+ if isinstance(left, nodes.BoolLit) and isinstance(right, nodes.BoolLit):
88
+ result = self._fold_bool_binary(left.value, op, right.value, node)
89
+ if result is not None:
90
+ return result
91
+
92
+ # String concatenation
93
+ if isinstance(left, nodes.StringLit) and isinstance(right, nodes.StringLit):
94
+ if op == "+":
95
+ return nodes.StringLit(
96
+ value=left.value + right.value,
97
+ source_location=node.source_location,
98
+ )
99
+
100
+ # Cannot fold, return transformed node
101
+ return node
102
+
103
+ def transform_unary_expr(self, node: nodes.UnaryExprNode) -> nodes.ASTNode:
104
+ """
105
+ Transform unary expressions, folding constants where possible.
106
+
107
+ Handles:
108
+ - Negation of numeric literals: -5 -> IntLit(-5)
109
+ - Logical not of booleans: !TRUE -> FALSE
110
+ """
111
+ # First, transform the operand
112
+ node = super().transform_unary_expr(node)
113
+
114
+ operand = node.operand
115
+ op = node.operator
116
+
117
+ # Numeric negation
118
+ if op == "-":
119
+ if isinstance(operand, nodes.IntLit):
120
+ return nodes.IntLit(
121
+ value=-operand.value,
122
+ source_location=node.source_location,
123
+ )
124
+ if isinstance(operand, nodes.FloatLit):
125
+ return nodes.FloatLit(
126
+ value=-operand.value,
127
+ source_location=node.source_location,
128
+ )
129
+
130
+ # Logical not
131
+ if op == "!":
132
+ if isinstance(operand, nodes.BoolLit):
133
+ return nodes.BoolLit(
134
+ value=not operand.value,
135
+ source_location=node.source_location,
136
+ )
137
+
138
+ # Cannot fold, return transformed node
139
+ return node
140
+
141
+ def _is_numeric_literal(self, node: nodes.ASTNode) -> bool:
142
+ """Check if a node is a numeric literal (int or float)."""
143
+ return isinstance(node, (nodes.IntLit, nodes.FloatLit))
144
+
145
+ def _get_numeric_value(self, node: nodes.ASTNode) -> Union[int, float]:
146
+ """Extract numeric value from an IntLit or FloatLit."""
147
+ if isinstance(node, nodes.IntLit):
148
+ return node.value
149
+ if isinstance(node, nodes.FloatLit):
150
+ return node.value
151
+ raise TypeError(f"Expected numeric literal, got {type(node).__name__}")
152
+
153
+ def _fold_int_binary(
154
+ self,
155
+ left: int,
156
+ op: str,
157
+ right: int,
158
+ node: nodes.BinaryExprNode,
159
+ ) -> Optional[nodes.ASTNode]:
160
+ """Fold binary operations on two integers."""
161
+ loc = node.source_location
162
+
163
+ # Arithmetic operations
164
+ if op == "+":
165
+ return nodes.IntLit(value=left + right, source_location=loc)
166
+ if op == "-":
167
+ return nodes.IntLit(value=left - right, source_location=loc)
168
+ if op == "*":
169
+ return nodes.IntLit(value=left * right, source_location=loc)
170
+ if op == "/":
171
+ if right == 0:
172
+ if self.fold_division_by_zero:
173
+ raise ConstantFoldingError(
174
+ f"Division by zero at {loc}" if loc else "Division by zero"
175
+ )
176
+ return None # Leave for runtime
177
+ # Integer division
178
+ return nodes.IntLit(value=left // right, source_location=loc)
179
+
180
+ # Comparison operations
181
+ if op == "==":
182
+ return nodes.BoolLit(value=left == right, source_location=loc)
183
+ if op == "!=":
184
+ return nodes.BoolLit(value=left != right, source_location=loc)
185
+ if op == "<":
186
+ return nodes.BoolLit(value=left < right, source_location=loc)
187
+ if op == ">":
188
+ return nodes.BoolLit(value=left > right, source_location=loc)
189
+ if op == "<=":
190
+ return nodes.BoolLit(value=left <= right, source_location=loc)
191
+ if op == ">=":
192
+ return nodes.BoolLit(value=left >= right, source_location=loc)
193
+
194
+ return None
195
+
196
+ def _fold_float_binary(
197
+ self,
198
+ left: Union[int, float],
199
+ op: str,
200
+ right: Union[int, float],
201
+ node: nodes.BinaryExprNode,
202
+ ) -> Optional[nodes.ASTNode]:
203
+ """Fold binary operations involving at least one float."""
204
+ loc = node.source_location
205
+
206
+ # If both are actually ints, let _fold_int_binary handle it
207
+ if isinstance(left, int) and isinstance(right, int):
208
+ return None
209
+
210
+ # Arithmetic operations (result is float)
211
+ if op == "+":
212
+ return nodes.FloatLit(value=float(left + right), source_location=loc)
213
+ if op == "-":
214
+ return nodes.FloatLit(value=float(left - right), source_location=loc)
215
+ if op == "*":
216
+ return nodes.FloatLit(value=float(left * right), source_location=loc)
217
+ if op == "/":
218
+ if right == 0:
219
+ if self.fold_division_by_zero:
220
+ raise ConstantFoldingError(
221
+ f"Division by zero at {loc}" if loc else "Division by zero"
222
+ )
223
+ return None # Leave for runtime
224
+ return nodes.FloatLit(value=float(left) / float(right), source_location=loc)
225
+
226
+ # Comparison operations (result is bool)
227
+ if op == "==":
228
+ return nodes.BoolLit(value=left == right, source_location=loc)
229
+ if op == "!=":
230
+ return nodes.BoolLit(value=left != right, source_location=loc)
231
+ if op == "<":
232
+ return nodes.BoolLit(value=left < right, source_location=loc)
233
+ if op == ">":
234
+ return nodes.BoolLit(value=left > right, source_location=loc)
235
+ if op == "<=":
236
+ return nodes.BoolLit(value=left <= right, source_location=loc)
237
+ if op == ">=":
238
+ return nodes.BoolLit(value=left >= right, source_location=loc)
239
+
240
+ return None
241
+
242
+ def _fold_bool_binary(
243
+ self,
244
+ left: bool,
245
+ op: str,
246
+ right: bool,
247
+ node: nodes.BinaryExprNode,
248
+ ) -> Optional[nodes.ASTNode]:
249
+ """Fold binary operations on two booleans."""
250
+ loc = node.source_location
251
+
252
+ if op == "&&":
253
+ return nodes.BoolLit(value=left and right, source_location=loc)
254
+ if op == "||":
255
+ return nodes.BoolLit(value=left or right, source_location=loc)
256
+ if op == "==":
257
+ return nodes.BoolLit(value=left == right, source_location=loc)
258
+ if op == "!=":
259
+ return nodes.BoolLit(value=left != right, source_location=loc)
260
+
261
+ return None
262
+
263
+
264
+ def fold_constants(ast: nodes.ASTNode, **kwargs) -> nodes.ASTNode:
265
+ """
266
+ Convenience function to fold constants in an AST.
267
+
268
+ Args:
269
+ ast: The AST to transform.
270
+ **kwargs: Options passed to ConstantFolder constructor.
271
+
272
+ Returns:
273
+ A new AST with constant expressions folded.
274
+
275
+ Example:
276
+ from yuho.ast.constant_folder import fold_constants
277
+ optimized = fold_constants(parsed_ast)
278
+ """
279
+ folder = ConstantFolder(**kwargs)
280
+ return folder.transform(ast)
yuho/ast/dead_code.py ADDED
@@ -0,0 +1,199 @@
1
+ """
2
+ Dead code elimination for Yuho AST.
3
+
4
+ Removes provably unreachable code branches:
5
+ - Match arms that can never be reached (covered by earlier patterns)
6
+ - Branches guarded by constant FALSE conditions
7
+ - Entire match expressions that reduce to a single arm
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ from typing import List, Optional, Set
12
+
13
+ from yuho.ast import nodes
14
+ from yuho.ast.transformer import Transformer
15
+ from yuho.ast.constant_folder import ConstantFolder
16
+ from yuho.ast.reachability import ReachabilityChecker, ReachabilityResult
17
+ from yuho.ast.type_inference import TypeInferenceResult
18
+
19
+
20
+ @dataclass
21
+ class EliminationStats:
22
+ """Statistics about eliminated dead code."""
23
+
24
+ removed_match_arms: int = 0
25
+ removed_true_guards: int = 0
26
+ simplified_matches: int = 0
27
+
28
+ @property
29
+ def total_eliminations(self) -> int:
30
+ return self.removed_match_arms + self.removed_true_guards + self.simplified_matches
31
+
32
+
33
+ class DeadCodeEliminator(Transformer):
34
+ """
35
+ AST transformer that removes provably unreachable branches.
36
+
37
+ Performs the following optimizations:
38
+ 1. Removes match arms that are unreachable (covered by earlier patterns)
39
+ 2. Removes guards that are constant TRUE (simplifies to unguarded arm)
40
+ 3. Removes entire match expressions that have only one reachable arm
41
+ 4. Removes branches guarded by FALSE conditions (not yet if-exprs exist)
42
+
43
+ Usage:
44
+ eliminator = DeadCodeEliminator()
45
+ optimized_ast = eliminator.transform(ast)
46
+ print(f"Removed {eliminator.stats.total_eliminations} dead code items")
47
+
48
+ Note:
49
+ This transformer should typically be run after constant folding
50
+ to maximize dead code detection.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ type_info: Optional[TypeInferenceResult] = None,
56
+ *,
57
+ fold_constants_first: bool = True,
58
+ ):
59
+ """
60
+ Initialize the dead code eliminator.
61
+
62
+ Args:
63
+ type_info: Type inference result for better reachability analysis.
64
+ fold_constants_first: If True, fold constants before elimination
65
+ to maximize dead code detection.
66
+ """
67
+ self.type_info = type_info
68
+ self.fold_constants_first = fold_constants_first
69
+ self.stats = EliminationStats()
70
+ self._constant_folder = ConstantFolder() if fold_constants_first else None
71
+
72
+ def transform(self, node: nodes.ASTNode) -> nodes.ASTNode:
73
+ """
74
+ Transform the AST, eliminating dead code.
75
+
76
+ If fold_constants_first is True, runs constant folding pass first.
77
+ """
78
+ if self._constant_folder:
79
+ node = self._constant_folder.transform(node)
80
+ return super().transform(node)
81
+
82
+ def transform_match_expr(self, node: nodes.MatchExprNode) -> nodes.ASTNode:
83
+ """
84
+ Transform match expression, removing unreachable arms.
85
+
86
+ 1. Transform children first (recurse into arm bodies)
87
+ 2. Run reachability analysis
88
+ 3. Filter out unreachable arms
89
+ 4. If only one arm remains and it's a wildcard, simplify
90
+ """
91
+ # First, transform children (arm bodies, guards, etc.)
92
+ node = super().transform_match_expr(node)
93
+
94
+ # Run reachability check on this match
95
+ checker = ReachabilityChecker(self.type_info)
96
+ result = checker._check_match_reachability(node)
97
+
98
+ # Get indices of unreachable arms
99
+ unreachable_indices: Set[int] = {
100
+ arm.arm_index for arm in result.unreachable_arms
101
+ }
102
+
103
+ # Filter and transform arms
104
+ new_arms = []
105
+ for i, arm in enumerate(node.arms):
106
+ if i in unreachable_indices:
107
+ self.stats.removed_match_arms += 1
108
+ continue
109
+
110
+ # Check if guard is constant TRUE - can remove guard
111
+ new_arm = self._simplify_arm_guard(arm)
112
+ new_arms.append(new_arm)
113
+
114
+ # If no arms remain (shouldn't happen in valid code), return unchanged
115
+ if not new_arms:
116
+ return node
117
+
118
+ # If only one arm remains and it's a wildcard, simplify to just the body
119
+ if len(new_arms) == 1:
120
+ single_arm = new_arms[0]
121
+ if self._is_catch_all_arm(single_arm):
122
+ self.stats.simplified_matches += 1
123
+ # Return the body directly
124
+ return single_arm.body
125
+
126
+ # Check if arms changed
127
+ if len(new_arms) != len(node.arms):
128
+ return nodes.MatchExprNode(
129
+ scrutinee=node.scrutinee,
130
+ arms=tuple(new_arms),
131
+ ensure_exhaustiveness=node.ensure_exhaustiveness,
132
+ source_location=node.source_location,
133
+ )
134
+
135
+ return node
136
+
137
+ def _simplify_arm_guard(self, arm: nodes.MatchArm) -> nodes.MatchArm:
138
+ """
139
+ Simplify arm guard if it's a constant TRUE.
140
+
141
+ A guard that's always TRUE can be removed entirely.
142
+ """
143
+ if arm.guard is None:
144
+ return arm
145
+
146
+ # Check if guard is constant TRUE
147
+ guard = arm.guard
148
+ if self._constant_folder:
149
+ guard = self._constant_folder.transform(guard)
150
+
151
+ if isinstance(guard, nodes.BoolLit) and guard.value is True:
152
+ self.stats.removed_true_guards += 1
153
+ return nodes.MatchArm(
154
+ pattern=arm.pattern,
155
+ guard=None, # Remove the always-true guard
156
+ body=arm.body,
157
+ source_location=arm.source_location,
158
+ )
159
+
160
+ # Guard is constant FALSE - this arm is dead
161
+ # (but ReachabilityChecker should have caught this)
162
+
163
+ return arm
164
+
165
+ def _is_catch_all_arm(self, arm: nodes.MatchArm) -> bool:
166
+ """
167
+ Check if an arm is a catch-all (wildcard with no guard).
168
+
169
+ A catch-all arm matches any value unconditionally.
170
+ """
171
+ if arm.guard is not None:
172
+ return False
173
+
174
+ pattern = arm.pattern
175
+ return isinstance(pattern, (nodes.WildcardPattern, nodes.BindingPattern))
176
+
177
+
178
+ def eliminate_dead_code(
179
+ ast: nodes.ASTNode,
180
+ type_info: Optional[TypeInferenceResult] = None,
181
+ **kwargs,
182
+ ) -> nodes.ASTNode:
183
+ """
184
+ Convenience function to eliminate dead code from an AST.
185
+
186
+ Args:
187
+ ast: The AST to transform.
188
+ type_info: Optional type inference result for better analysis.
189
+ **kwargs: Options passed to DeadCodeEliminator constructor.
190
+
191
+ Returns:
192
+ A new AST with dead code eliminated.
193
+
194
+ Example:
195
+ from yuho.ast.dead_code import eliminate_dead_code
196
+ optimized = eliminate_dead_code(parsed_ast)
197
+ """
198
+ eliminator = DeadCodeEliminator(type_info, **kwargs)
199
+ return eliminator.transform(ast)