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,334 @@
1
+ """
2
+ AST visualization command for Yuho.
3
+
4
+ Displays statute AST structure as tree-like ASCII output in terminal.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Optional, List, Dict, Any, Tuple
9
+
10
+ import click
11
+
12
+
13
+ # Box-drawing characters for tree
14
+ TREE_CHARS = {
15
+ "pipe": "│",
16
+ "tee": "├",
17
+ "elbow": "└",
18
+ "dash": "─",
19
+ "space": " ",
20
+ }
21
+
22
+ # Minimal fallback for terminals without unicode
23
+ TREE_CHARS_ASCII = {
24
+ "pipe": "|",
25
+ "tee": "+",
26
+ "elbow": "`",
27
+ "dash": "-",
28
+ "space": " ",
29
+ }
30
+
31
+
32
+ class ASTVisualizer:
33
+ """Generates tree visualization of Yuho AST."""
34
+
35
+ def __init__(self, use_color: bool = True, use_unicode: bool = True):
36
+ self.use_color = use_color
37
+ self.chars = TREE_CHARS if use_unicode else TREE_CHARS_ASCII
38
+ self.lines: List[str] = []
39
+
40
+ def colorize(self, text: str, color: str, bold: bool = False) -> str:
41
+ """Apply color if enabled."""
42
+ if not self.use_color:
43
+ return text
44
+ return click.style(text, fg=color, bold=bold)
45
+
46
+ def _get_node_display(self, node: Any) -> Tuple[str, str]:
47
+ """
48
+ Get display text and color for a node.
49
+
50
+ Returns:
51
+ (display_text, color)
52
+ """
53
+ node_type = type(node).__name__
54
+
55
+ # Map node types to colors and display format
56
+ type_info = {
57
+ "Statute": ("cyan", True),
58
+ "Section": ("blue", True),
59
+ "Definitions": ("yellow", False),
60
+ "Definition": ("yellow", False),
61
+ "Elements": ("green", False),
62
+ "Element": ("green", False),
63
+ "ActusReus": ("green", False),
64
+ "MensRea": ("green", False),
65
+ "Circumstance": ("green", False),
66
+ "Penalty": ("red", False),
67
+ "PenaltyClause": ("red", False),
68
+ "Imprisonment": ("red", False),
69
+ "Fine": ("red", False),
70
+ "Illustrations": ("magenta", False),
71
+ "Illustration": ("magenta", False),
72
+ "Import": ("white", False),
73
+ "TypeDecl": ("cyan", False),
74
+ "FuncDecl": ("cyan", False),
75
+ "Block": ("white", False),
76
+ "Program": ("cyan", True),
77
+ }
78
+
79
+ color, bold = type_info.get(node_type, ("white", False))
80
+
81
+ # Build display text with node-specific info
82
+ display = node_type
83
+
84
+ if hasattr(node, "section_number"):
85
+ display = f"{node_type}[{node.section_number}]"
86
+ elif hasattr(node, "title") and node.title:
87
+ title = node.title[:30] + "..." if len(str(node.title)) > 30 else node.title
88
+ display = f"{node_type}: {title}"
89
+ elif hasattr(node, "name") and node.name:
90
+ display = f"{node_type}: {node.name}"
91
+ elif hasattr(node, "term") and node.term:
92
+ display = f"{node_type}: {node.term}"
93
+ elif hasattr(node, "label") and node.label:
94
+ display = f"{node_type}: {node.label}"
95
+ elif hasattr(node, "element_type"):
96
+ display = f"{node_type}({node.element_type})"
97
+
98
+ return display, color
99
+
100
+ def _get_children(self, node: Any) -> List[Tuple[str, Any]]:
101
+ """
102
+ Get child nodes with their attribute names.
103
+
104
+ Returns:
105
+ List of (attribute_name, child_node) tuples
106
+ """
107
+ children = []
108
+
109
+ # Known container attributes
110
+ container_attrs = [
111
+ "statutes", "sections", "statements", "items", "children",
112
+ "definitions", "elements", "illustrations", "penalties",
113
+ "clauses", "imports", "declarations", "blocks", "body",
114
+ ]
115
+
116
+ # Check for container attributes first
117
+ for attr in container_attrs:
118
+ if hasattr(node, attr):
119
+ items = getattr(node, attr)
120
+ if isinstance(items, (list, tuple)):
121
+ for i, item in enumerate(items):
122
+ if item is not None:
123
+ children.append((f"{attr}[{i}]", item))
124
+
125
+ # Check for single child attributes
126
+ single_attrs = [
127
+ "statute", "section", "penalty", "definition", "element",
128
+ "illustration", "actus_reus", "mens_rea", "circumstance",
129
+ "imprisonment", "fine", "supplementary", "condition",
130
+ "consequence", "left", "right", "value", "expression",
131
+ ]
132
+
133
+ for attr in single_attrs:
134
+ if hasattr(node, attr):
135
+ child = getattr(node, attr)
136
+ if child is not None and not isinstance(child, (str, int, float, bool)):
137
+ children.append((attr, child))
138
+
139
+ return children
140
+
141
+ def _render_node(
142
+ self,
143
+ node: Any,
144
+ prefix: str = "",
145
+ is_last: bool = True,
146
+ show_attr: str = "",
147
+ ) -> None:
148
+ """Recursively render a node and its children."""
149
+ # Determine connector
150
+ if prefix:
151
+ connector = self.chars["elbow"] if is_last else self.chars["tee"]
152
+ connector += self.chars["dash"] * 2 + " "
153
+ else:
154
+ connector = ""
155
+
156
+ # Get display text
157
+ display, color = self._get_node_display(node)
158
+
159
+ # Add attribute name if provided
160
+ if show_attr:
161
+ attr_text = self.colorize(f"{show_attr}: ", "white")
162
+ else:
163
+ attr_text = ""
164
+
165
+ # Build line
166
+ node_text = self.colorize(display, color, bold=(color == "cyan"))
167
+ self.lines.append(f"{prefix}{connector}{attr_text}{node_text}")
168
+
169
+ # Get children
170
+ children = self._get_children(node)
171
+
172
+ # Calculate new prefix for children
173
+ if prefix:
174
+ if is_last:
175
+ new_prefix = prefix + " "
176
+ else:
177
+ new_prefix = prefix + self.chars["pipe"] + " "
178
+ else:
179
+ new_prefix = ""
180
+
181
+ # Render children
182
+ for i, (attr, child) in enumerate(children):
183
+ is_last_child = (i == len(children) - 1)
184
+ self._render_node(child, new_prefix, is_last_child, attr)
185
+
186
+ def visualize(self, ast: Any) -> str:
187
+ """
188
+ Generate tree visualization of AST.
189
+
190
+ Args:
191
+ ast: The AST root node
192
+
193
+ Returns:
194
+ String containing the tree visualization
195
+ """
196
+ self.lines = []
197
+
198
+ # Handle list of nodes
199
+ if isinstance(ast, (list, tuple)):
200
+ for i, node in enumerate(ast):
201
+ is_last = (i == len(ast) - 1)
202
+ self._render_node(node, "", is_last)
203
+ else:
204
+ self._render_node(ast, "", True)
205
+
206
+ return "\n".join(self.lines)
207
+
208
+
209
+ def format_ast_stats(ast: Any) -> Dict[str, int]:
210
+ """
211
+ Collect statistics about the AST.
212
+
213
+ Returns:
214
+ Dict mapping node type to count
215
+ """
216
+ stats: Dict[str, int] = {}
217
+
218
+ def visit(node: Any) -> None:
219
+ if node is None:
220
+ return
221
+
222
+ if isinstance(node, (list, tuple)):
223
+ for item in node:
224
+ visit(item)
225
+ return
226
+
227
+ # Count this node type
228
+ node_type = type(node).__name__
229
+ stats[node_type] = stats.get(node_type, 0) + 1
230
+
231
+ # Visit all attributes that might contain children
232
+ for attr in dir(node):
233
+ if attr.startswith("_"):
234
+ continue
235
+ try:
236
+ value = getattr(node, attr)
237
+ if isinstance(value, (list, tuple)):
238
+ for item in value:
239
+ if hasattr(item, "__class__") and not isinstance(item, (str, int, float, bool)):
240
+ visit(item)
241
+ elif hasattr(value, "__class__") and not isinstance(value, (str, int, float, bool, type(None))):
242
+ if not callable(value):
243
+ visit(value)
244
+ except (AttributeError, TypeError):
245
+ pass
246
+
247
+ visit(ast)
248
+ return stats
249
+
250
+
251
+ def run_ast_viz(
252
+ file: str,
253
+ output: Optional[str] = None,
254
+ stats: bool = False,
255
+ depth: int = 0,
256
+ no_unicode: bool = False,
257
+ verbose: bool = False,
258
+ color: bool = True,
259
+ ) -> None:
260
+ """
261
+ Run AST visualization command.
262
+
263
+ Args:
264
+ file: Input .yh file
265
+ output: Output file path (None = stdout)
266
+ stats: Show statistics
267
+ depth: Max depth (0 = unlimited)
268
+ no_unicode: Use ASCII-only characters
269
+ verbose: Verbose output
270
+ color: Use colors
271
+ """
272
+ path = Path(file)
273
+
274
+ if not path.exists():
275
+ click.echo(f"Error: File not found: {file}", err=True)
276
+ raise SystemExit(1)
277
+
278
+ # Read source
279
+ source = path.read_text()
280
+
281
+ # Parse
282
+ try:
283
+ from yuho.parser.scanner import Scanner
284
+ from yuho.parser.parser import Parser
285
+ from yuho.ast.builder import ASTBuilder
286
+
287
+ scanner = Scanner(source)
288
+ tokens = scanner.scan_tokens()
289
+
290
+ parser = Parser(tokens)
291
+ tree = parser.parse()
292
+
293
+ builder = ASTBuilder()
294
+ ast = builder.build(tree)
295
+
296
+ except Exception as e:
297
+ click.echo(f"Parse error: {e}", err=True)
298
+ raise SystemExit(1)
299
+
300
+ # Visualize
301
+ visualizer = ASTVisualizer(use_color=color, use_unicode=not no_unicode)
302
+ tree_output = visualizer.visualize(ast)
303
+
304
+ # Build output
305
+ lines = []
306
+
307
+ # Header
308
+ if verbose:
309
+ lines.append(click.style(f"AST for: {path.name}", fg="cyan", bold=True))
310
+ lines.append(click.style("=" * 50, fg="cyan"))
311
+ lines.append("")
312
+
313
+ lines.append(tree_output)
314
+
315
+ # Stats
316
+ if stats:
317
+ lines.append("")
318
+ lines.append(click.style("Statistics:", fg="yellow", bold=True))
319
+ lines.append(click.style("-" * 30, fg="yellow"))
320
+
321
+ ast_stats = format_ast_stats(ast)
322
+ for node_type, count in sorted(ast_stats.items()):
323
+ lines.append(f" {node_type}: {count}")
324
+ lines.append(f" {'─' * 20}")
325
+ lines.append(f" Total nodes: {sum(ast_stats.values())}")
326
+
327
+ result = "\n".join(lines)
328
+
329
+ # Output
330
+ if output:
331
+ Path(output).write_text(result)
332
+ click.echo(f"Written to {output}")
333
+ else:
334
+ click.echo(result)
@@ -0,0 +1,218 @@
1
+ """
2
+ Check command - parse and validate Yuho files.
3
+ """
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import click
11
+
12
+ from yuho.parser import Parser
13
+ from yuho.ast import ASTBuilder
14
+ from yuho.cli.error_formatter import format_errors, format_suggestion, Colors, colorize
15
+
16
+
17
+ # Detailed explanations for common error patterns
18
+ ERROR_EXPLANATIONS = {
19
+ "Unexpected syntax": """
20
+ This error occurs when the parser encounters code it doesn't recognize.
21
+
22
+ Common causes:
23
+ - Missing or extra punctuation (commas, braces, colons)
24
+ - Typo in a keyword (e.g., 'strcut' instead of 'struct')
25
+ - Using syntax from another language that Yuho doesn't support
26
+ - Incomplete statement or expression
27
+
28
+ How to fix:
29
+ 1. Check for typos in keywords: struct, fn, match, case, consequence, etc.
30
+ 2. Ensure all braces {{ }} and parentheses ( ) are balanced
31
+ 3. Verify that statements are complete (not cut off mid-expression)
32
+ 4. Compare your code against examples in the documentation
33
+ """,
34
+ "Missing semicolon": """
35
+ Yuho doesn't require semicolons in most places, but they can be used
36
+ optionally at the end of statements.
37
+
38
+ This error usually means the parser expected a statement terminator
39
+ but found something else. Check that:
40
+ - The previous statement is complete
41
+ - You're not missing a closing brace or parenthesis
42
+ - Field assignments in structs are separated by commas
43
+ """,
44
+ "Missing closing brace": """
45
+ A closing brace '}}' is expected but was not found.
46
+
47
+ Common causes:
48
+ - Forgetting to close a struct, function, or match block
49
+ - Nested braces that don't match up
50
+ - An error earlier in the file causing the parser to get confused
51
+
52
+ How to fix:
53
+ 1. Count your opening {{ and closing }} braces - they should match
54
+ 2. Use editor features to highlight matching braces
55
+ 3. Look for the last complete block before the error
56
+ """,
57
+ "Expected identifier": """
58
+ An identifier (name) was expected but something else was found.
59
+
60
+ In Yuho, identifiers:
61
+ - Start with a letter or underscore
62
+ - Can contain letters, numbers, and underscores
63
+ - Are case-sensitive
64
+ - Cannot be reserved keywords
65
+
66
+ Examples of valid identifiers: myVar, _private, CamelCase, snake_case
67
+ """,
68
+ "Expected type annotation": """
69
+ A type annotation was expected but not found.
70
+
71
+ In Yuho, variable declarations require explicit types:
72
+
73
+ Correct: int count := 0
74
+ Incorrect: count := 0
75
+
76
+ Built-in types: int, float, bool, string, money, percent, date, duration
77
+ User-defined types: Any struct name you've defined
78
+ """,
79
+ "MISSING": """
80
+ The parser expected to find something that wasn't there.
81
+
82
+ This usually indicates incomplete code. Check that:
83
+ - All statements and expressions are complete
84
+ - Required parts of syntax aren't missing
85
+ - The file didn't get truncated
86
+ """,
87
+ }
88
+
89
+
90
+ def get_error_explanation(error_message: str, node_type: Optional[str]) -> Optional[str]:
91
+ """Get a detailed explanation for an error."""
92
+ # Check node type first
93
+ if node_type and node_type.startswith("MISSING:"):
94
+ missing_what = node_type.replace("MISSING:", "")
95
+ if missing_what in ERROR_EXPLANATIONS:
96
+ return ERROR_EXPLANATIONS[missing_what]
97
+ return ERROR_EXPLANATIONS.get("MISSING")
98
+
99
+ # Check error message patterns
100
+ for pattern, explanation in ERROR_EXPLANATIONS.items():
101
+ if pattern in error_message:
102
+ return explanation
103
+
104
+ return None
105
+
106
+
107
+ def run_check(
108
+ file: str,
109
+ json_output: bool = False,
110
+ verbose: bool = False,
111
+ explain_errors: bool = False,
112
+ ) -> None:
113
+ """
114
+ Parse and validate a Yuho source file.
115
+
116
+ Args:
117
+ file: Path to the .yh file
118
+ json_output: Output errors as JSON
119
+ verbose: Enable verbose output
120
+ explain_errors: Show detailed explanations for errors
121
+ """
122
+ file_path = Path(file)
123
+
124
+ if verbose:
125
+ click.echo(f"Checking {file_path}...")
126
+
127
+ # Parse the file
128
+ parser = Parser()
129
+ try:
130
+ result = parser.parse_file(file_path)
131
+ except FileNotFoundError:
132
+ if json_output:
133
+ print(json.dumps({"valid": False, "errors": [{"message": f"File not found: {file}"}]}))
134
+ else:
135
+ click.echo(colorize(f"error: File not found: {file}", Colors.RED), err=True)
136
+ sys.exit(1)
137
+ except UnicodeDecodeError as e:
138
+ if json_output:
139
+ print(json.dumps({"valid": False, "errors": [{"message": f"Invalid UTF-8: {e}"}]}))
140
+ else:
141
+ click.echo(colorize(f"error: Invalid UTF-8 encoding: {e}", Colors.RED), err=True)
142
+ sys.exit(1)
143
+
144
+ # Report parse errors
145
+ if result.errors:
146
+ if json_output:
147
+ errors_json = [
148
+ {
149
+ "message": err.message,
150
+ "location": {
151
+ "file": err.location.file,
152
+ "line": err.location.line,
153
+ "col": err.location.col,
154
+ "end_line": err.location.end_line,
155
+ "end_col": err.location.end_col,
156
+ },
157
+ "node_type": err.node_type,
158
+ "explanation": get_error_explanation(err.message, err.node_type) if explain_errors else None,
159
+ }
160
+ for err in result.errors
161
+ ]
162
+ print(json.dumps({"valid": False, "errors": errors_json}, indent=2))
163
+ else:
164
+ error_output = format_errors(result.errors, result.source, str(file_path))
165
+ click.echo(error_output, err=True)
166
+
167
+ # Add suggestions
168
+ for err in result.errors:
169
+ suggestion = format_suggestion(err, result.source)
170
+ if suggestion:
171
+ click.echo(colorize(f" hint: {suggestion}", Colors.YELLOW), err=True)
172
+
173
+ # Add detailed explanation if requested
174
+ if explain_errors:
175
+ explanation = get_error_explanation(err.message, err.node_type)
176
+ if explanation:
177
+ click.echo(colorize("\n Explanation:", Colors.CYAN + Colors.BOLD), err=True)
178
+ for line in explanation.strip().split("\n"):
179
+ click.echo(colorize(f" {line}", Colors.DIM), err=True)
180
+ click.echo("", err=True)
181
+
182
+ sys.exit(1)
183
+
184
+ # Build AST
185
+ try:
186
+ builder = ASTBuilder(result.source, str(file_path))
187
+ ast = builder.build(result.root_node)
188
+ except Exception as e:
189
+ if json_output:
190
+ print(json.dumps({"valid": False, "errors": [{"message": f"AST error: {e}"}]}))
191
+ else:
192
+ click.echo(colorize(f"error: Failed to build AST: {e}", Colors.RED), err=True)
193
+ sys.exit(1)
194
+
195
+ # Success
196
+ if json_output:
197
+ summary = {
198
+ "valid": True,
199
+ "file": str(file_path),
200
+ "stats": {
201
+ "imports": len(ast.imports),
202
+ "structs": len(ast.type_defs),
203
+ "functions": len(ast.function_defs),
204
+ "statutes": len(ast.statutes),
205
+ "variables": len(ast.variables),
206
+ }
207
+ }
208
+ print(json.dumps(summary, indent=2))
209
+ else:
210
+ click.echo(colorize(f"OK: {file_path}", Colors.CYAN + Colors.BOLD))
211
+ if verbose:
212
+ click.echo(f" {len(ast.imports)} imports")
213
+ click.echo(f" {len(ast.type_defs)} type definitions")
214
+ click.echo(f" {len(ast.function_defs)} functions")
215
+ click.echo(f" {len(ast.statutes)} statutes")
216
+ click.echo(f" {len(ast.variables)} variables")
217
+
218
+ sys.exit(0)