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,240 @@
1
+ """
2
+ Explain command - LLM-powered explanations of Yuho files.
3
+ """
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import click
10
+
11
+ from yuho.parser import Parser
12
+ from yuho.ast import ASTBuilder
13
+ from yuho.transpile import EnglishTranspiler
14
+ from yuho.cli.error_formatter import Colors, colorize
15
+
16
+
17
+ def run_explain(
18
+ file: str,
19
+ section: Optional[str] = None,
20
+ interactive: bool = False,
21
+ provider: Optional[str] = None,
22
+ model: Optional[str] = None,
23
+ verbose: bool = False,
24
+ stream: bool = True,
25
+ ) -> None:
26
+ """
27
+ Generate natural language explanation of a Yuho file.
28
+
29
+ Args:
30
+ file: Path to the .yh file
31
+ section: Specific section to explain
32
+ interactive: Enable interactive REPL mode
33
+ provider: LLM provider (ollama, huggingface, openai, anthropic)
34
+ model: Model name
35
+ verbose: Enable verbose output
36
+ stream: Enable streaming output for real-time response
37
+ """
38
+ file_path = Path(file)
39
+
40
+ # Parse and build AST
41
+ parser = Parser()
42
+ try:
43
+ result = parser.parse_file(file_path)
44
+ except FileNotFoundError:
45
+ click.echo(colorize(f"error: File not found: {file}", Colors.RED), err=True)
46
+ sys.exit(1)
47
+
48
+ if result.errors:
49
+ click.echo(colorize(f"error: Parse errors in {file}", Colors.RED), err=True)
50
+ sys.exit(1)
51
+
52
+ builder = ASTBuilder(result.source, str(file_path))
53
+ ast = builder.build(result.root_node)
54
+
55
+ # Filter to specific section if requested
56
+ if section:
57
+ matching = [s for s in ast.statutes if section in s.section_number]
58
+ if not matching:
59
+ click.echo(colorize(f"error: Section {section} not found", Colors.RED), err=True)
60
+ sys.exit(1)
61
+
62
+ # Create filtered module
63
+ from yuho.ast.nodes import ModuleNode
64
+ ast = ModuleNode(
65
+ imports=ast.imports,
66
+ type_defs=ast.type_defs,
67
+ function_defs=ast.function_defs,
68
+ statutes=tuple(matching),
69
+ variables=ast.variables,
70
+ source_location=ast.source_location,
71
+ )
72
+
73
+ # Generate base English explanation
74
+ english = EnglishTranspiler()
75
+ base_explanation = english.transpile(ast)
76
+
77
+ if interactive:
78
+ _run_interactive(base_explanation, ast, provider, model, stream)
79
+ else:
80
+ # Try to use LLM for enhanced explanation
81
+ try:
82
+ if stream:
83
+ _enhance_with_llm_stream(base_explanation, provider, model, verbose)
84
+ else:
85
+ enhanced = _enhance_with_llm(base_explanation, provider, model, verbose)
86
+ click.echo(enhanced)
87
+ except Exception as e:
88
+ if verbose:
89
+ click.echo(colorize(f"LLM unavailable: {e}", Colors.YELLOW), err=True)
90
+ # Fall back to basic English transpilation
91
+ click.echo(base_explanation)
92
+
93
+
94
+ def _enhance_with_llm(text: str, provider: Optional[str], model: Optional[str], verbose: bool) -> str:
95
+ """Use LLM to enhance the explanation."""
96
+ try:
97
+ from yuho.llm import get_provider, LLMConfig
98
+ except ImportError:
99
+ return text # LLM module not available
100
+
101
+ try:
102
+ # Build config
103
+ config = LLMConfig(
104
+ provider=provider or "ollama",
105
+ model_name=model or "llama3",
106
+ )
107
+ llm = get_provider(config)
108
+
109
+ prompt = f"""You are a legal expert explaining statute provisions to a general audience.
110
+ Given the following structured explanation of a legal statute, rewrite it in clear,
111
+ accessible language that a non-lawyer could understand. Keep the legal accuracy but
112
+ make it readable.
113
+
114
+ Structured explanation:
115
+ {text}
116
+
117
+ Clear explanation:"""
118
+
119
+ return llm.generate(prompt, max_tokens=2000)
120
+
121
+ except Exception as e:
122
+ if verbose:
123
+ click.echo(colorize(f"LLM error: {e}", Colors.YELLOW), err=True)
124
+ return text
125
+
126
+
127
+ def _enhance_with_llm_stream(text: str, provider: Optional[str], model: Optional[str], verbose: bool) -> None:
128
+ """Use LLM to enhance the explanation with streaming output."""
129
+ try:
130
+ from yuho.llm import get_provider, LLMConfig
131
+ except ImportError:
132
+ click.echo(text) # LLM module not available
133
+ return
134
+
135
+ try:
136
+ # Build config
137
+ config = LLMConfig(
138
+ provider=provider or "ollama",
139
+ model_name=model or "llama3",
140
+ )
141
+ llm = get_provider(config)
142
+
143
+ prompt = f"""You are a legal expert explaining statute provisions to a general audience.
144
+ Given the following structured explanation of a legal statute, rewrite it in clear,
145
+ accessible language that a non-lawyer could understand. Keep the legal accuracy but
146
+ make it readable.
147
+
148
+ Structured explanation:
149
+ {text}
150
+
151
+ Clear explanation:"""
152
+
153
+ # Check if provider supports streaming
154
+ if hasattr(llm, 'stream'):
155
+ # Stream tokens to stdout
156
+ for token in llm.stream(prompt, max_tokens=2000):
157
+ click.echo(token, nl=False)
158
+ sys.stdout.flush()
159
+ click.echo() # Final newline
160
+ else:
161
+ # Fall back to non-streaming
162
+ response = llm.generate(prompt, max_tokens=2000)
163
+ click.echo(response)
164
+
165
+ except Exception as e:
166
+ if verbose:
167
+ click.echo(colorize(f"LLM streaming error: {e}", Colors.YELLOW), err=True)
168
+ # Fall back to basic explanation
169
+ click.echo(text)
170
+
171
+
172
+ def _run_interactive(
173
+ base_explanation: str,
174
+ ast,
175
+ provider: Optional[str],
176
+ model: Optional[str],
177
+ stream: bool = True,
178
+ ) -> None:
179
+ """Run interactive REPL for follow-up questions."""
180
+ click.echo(colorize("Yuho Explain - Interactive Mode", Colors.CYAN + Colors.BOLD))
181
+ click.echo(colorize("Type 'quit' or 'exit' to leave. Type 'show' to see the statute.\n", Colors.DIM))
182
+
183
+ # Show initial explanation
184
+ click.echo(base_explanation)
185
+ click.echo()
186
+
187
+ try:
188
+ from yuho.llm import get_provider, LLMConfig
189
+ config = LLMConfig(provider=provider or "ollama", model_name=model or "llama3")
190
+ llm = get_provider(config)
191
+ has_llm = True
192
+ can_stream = stream and hasattr(llm, 'stream')
193
+ except Exception:
194
+ has_llm = False
195
+ can_stream = False
196
+ click.echo(colorize("(LLM not available - limited to basic queries)", Colors.YELLOW))
197
+
198
+ context = base_explanation
199
+
200
+ while True:
201
+ try:
202
+ query = click.prompt(colorize("?", Colors.CYAN), default="", show_default=False)
203
+ except (EOFError, KeyboardInterrupt):
204
+ click.echo("\nGoodbye!")
205
+ break
206
+
207
+ query = query.strip()
208
+ if not query:
209
+ continue
210
+
211
+ if query.lower() in ("quit", "exit", "q"):
212
+ click.echo("Goodbye!")
213
+ break
214
+
215
+ if query.lower() == "show":
216
+ click.echo(base_explanation)
217
+ continue
218
+
219
+ if has_llm:
220
+ prompt = f"""Context about the statute:
221
+ {context}
222
+
223
+ User question: {query}
224
+
225
+ Provide a helpful, accurate answer:"""
226
+
227
+ try:
228
+ if can_stream:
229
+ click.echo() # Start on new line
230
+ for token in llm.stream(prompt, max_tokens=1000):
231
+ click.echo(token, nl=False)
232
+ sys.stdout.flush()
233
+ click.echo("\n") # End with newlines
234
+ else:
235
+ response = llm.generate(prompt, max_tokens=1000)
236
+ click.echo(f"\n{response}\n")
237
+ except Exception as e:
238
+ click.echo(colorize(f"Error: {e}", Colors.RED))
239
+ else:
240
+ click.echo(colorize("Cannot answer - LLM not available", Colors.YELLOW))
@@ -0,0 +1,253 @@
1
+ """
2
+ Format command - canonical formatting for Yuho files.
3
+ """
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import click
10
+
11
+ from yuho.parser import Parser
12
+ from yuho.ast import ASTBuilder
13
+ from yuho.cli.error_formatter import Colors, colorize
14
+
15
+
16
+ def run_fmt(file: str, in_place: bool = False, check: bool = False, verbose: bool = False) -> None:
17
+ """
18
+ Format a Yuho source file.
19
+
20
+ Args:
21
+ file: Path to the .yh file
22
+ in_place: Format file in place
23
+ check: Check if formatted (exit 1 if not)
24
+ verbose: Enable verbose output
25
+ """
26
+ file_path = Path(file)
27
+
28
+ # Parse file
29
+ parser = Parser()
30
+ try:
31
+ result = parser.parse_file(file_path)
32
+ except FileNotFoundError:
33
+ click.echo(colorize(f"error: File not found: {file}", Colors.RED), err=True)
34
+ sys.exit(1)
35
+
36
+ if result.errors:
37
+ click.echo(colorize(f"error: Cannot format file with parse errors", Colors.RED), err=True)
38
+ sys.exit(1)
39
+
40
+ # Build AST
41
+ builder = ASTBuilder(result.source, str(file_path))
42
+ ast = builder.build(result.root_node)
43
+
44
+ # Format using canonical printer
45
+ formatted = _format_module(ast)
46
+
47
+ # Compare with original
48
+ original = result.source
49
+ is_formatted = original.strip() == formatted.strip()
50
+
51
+ if check:
52
+ if is_formatted:
53
+ if verbose:
54
+ click.echo(f"OK: {file_path} is formatted")
55
+ sys.exit(0)
56
+ else:
57
+ click.echo(colorize(f"FAIL: {file_path} needs formatting", Colors.RED))
58
+ sys.exit(1)
59
+
60
+ if in_place:
61
+ if is_formatted:
62
+ if verbose:
63
+ click.echo(f"No changes: {file_path}")
64
+ else:
65
+ file_path.write_text(formatted, encoding="utf-8")
66
+ click.echo(f"Formatted: {file_path}")
67
+ else:
68
+ print(formatted)
69
+
70
+
71
+ def _format_module(ast) -> str:
72
+ """Format a module AST to canonical string representation."""
73
+ from yuho.ast import nodes
74
+
75
+ lines = []
76
+
77
+ # Imports
78
+ for imp in ast.imports:
79
+ if imp.is_wildcard:
80
+ lines.append(f'import * from "{imp.path}"')
81
+ elif imp.imported_names:
82
+ names = ", ".join(imp.imported_names)
83
+ lines.append(f'import {{ {names} }} from "{imp.path}"')
84
+ else:
85
+ lines.append(f'import "{imp.path}"')
86
+
87
+ if ast.imports:
88
+ lines.append("")
89
+
90
+ # Struct definitions
91
+ for struct in ast.type_defs:
92
+ lines.append(f"struct {struct.name} {{")
93
+ for field in struct.fields:
94
+ type_str = _format_type(field.type_annotation)
95
+ lines.append(f" {type_str} {field.name},")
96
+ lines.append("}")
97
+ lines.append("")
98
+
99
+ # Function definitions
100
+ for func in ast.function_defs:
101
+ params = ", ".join(
102
+ f"{_format_type(p.type_annotation)} {p.name}"
103
+ for p in func.params
104
+ )
105
+ ret = f": {_format_type(func.return_type)}" if func.return_type else ""
106
+ lines.append(f"fn {func.name}({params}){ret} {{")
107
+ for stmt in func.body.statements:
108
+ lines.append(f" {_format_statement(stmt)}")
109
+ lines.append("}")
110
+ lines.append("")
111
+
112
+ # Statutes
113
+ for statute in ast.statutes:
114
+ title = f' "{statute.title.value}"' if statute.title else ""
115
+ lines.append(f"statute {statute.section_number}{title} {{")
116
+
117
+ if statute.definitions:
118
+ lines.append(" definitions {")
119
+ for defn in statute.definitions:
120
+ lines.append(f' {defn.term} := "{defn.definition.value}";')
121
+ lines.append(" }")
122
+ lines.append("")
123
+
124
+ if statute.elements:
125
+ lines.append(" elements {")
126
+ for elem in statute.elements:
127
+ desc = _format_expr(elem.description)
128
+ lines.append(f" {elem.element_type} {elem.name} := {desc};")
129
+ lines.append(" }")
130
+ lines.append("")
131
+
132
+ if statute.penalty:
133
+ lines.append(" penalty {")
134
+ if statute.penalty.imprisonment_max:
135
+ dur = _format_duration(statute.penalty.imprisonment_max)
136
+ if statute.penalty.imprisonment_min:
137
+ min_dur = _format_duration(statute.penalty.imprisonment_min)
138
+ lines.append(f" imprisonment := {min_dur} .. {dur};")
139
+ else:
140
+ lines.append(f" imprisonment := {dur};")
141
+ if statute.penalty.fine_max:
142
+ money = _format_money(statute.penalty.fine_max)
143
+ if statute.penalty.fine_min:
144
+ min_money = _format_money(statute.penalty.fine_min)
145
+ lines.append(f" fine := {min_money} .. {money};")
146
+ else:
147
+ lines.append(f" fine := {money};")
148
+ if statute.penalty.supplementary:
149
+ lines.append(f' supplementary := "{statute.penalty.supplementary.value}";')
150
+ lines.append(" }")
151
+ lines.append("")
152
+
153
+ for illus in statute.illustrations:
154
+ label = illus.label or ""
155
+ lines.append(f' illustration {label} {{')
156
+ lines.append(f' "{illus.description.value}"')
157
+ lines.append(" }")
158
+ lines.append("")
159
+
160
+ lines.append("}")
161
+ lines.append("")
162
+
163
+ # Variables
164
+ for var in ast.variables:
165
+ type_str = _format_type(var.type_annotation)
166
+ if var.value:
167
+ val = _format_expr(var.value)
168
+ lines.append(f"{type_str} {var.name} := {val};")
169
+ else:
170
+ lines.append(f"{type_str} {var.name};")
171
+
172
+ return "\n".join(lines)
173
+
174
+
175
+ def _format_type(typ) -> str:
176
+ """Format a type node."""
177
+ from yuho.ast import nodes
178
+
179
+ if isinstance(typ, nodes.BuiltinType):
180
+ return typ.name
181
+ elif isinstance(typ, nodes.NamedType):
182
+ return typ.name
183
+ elif isinstance(typ, nodes.OptionalType):
184
+ return f"{_format_type(typ.inner)}?"
185
+ elif isinstance(typ, nodes.ArrayType):
186
+ return f"[{_format_type(typ.element_type)}]"
187
+ elif isinstance(typ, nodes.GenericType):
188
+ args = ", ".join(_format_type(a) for a in typ.type_args)
189
+ return f"{typ.base}<{args}>"
190
+ return "?"
191
+
192
+
193
+ def _format_expr(expr) -> str:
194
+ """Format an expression node."""
195
+ from yuho.ast import nodes
196
+
197
+ if isinstance(expr, nodes.IntLit):
198
+ return str(expr.value)
199
+ elif isinstance(expr, nodes.FloatLit):
200
+ return str(expr.value)
201
+ elif isinstance(expr, nodes.BoolLit):
202
+ return "TRUE" if expr.value else "FALSE"
203
+ elif isinstance(expr, nodes.StringLit):
204
+ return f'"{expr.value}"'
205
+ elif isinstance(expr, nodes.MoneyNode):
206
+ return _format_money(expr)
207
+ elif isinstance(expr, nodes.PercentNode):
208
+ return f"{expr.value}%"
209
+ elif isinstance(expr, nodes.DateNode):
210
+ return expr.value.isoformat()
211
+ elif isinstance(expr, nodes.DurationNode):
212
+ return _format_duration(expr)
213
+ elif isinstance(expr, nodes.IdentifierNode):
214
+ return expr.name
215
+ elif isinstance(expr, nodes.FieldAccessNode):
216
+ return f"{_format_expr(expr.base)}.{expr.field_name}"
217
+ elif isinstance(expr, nodes.BinaryExprNode):
218
+ return f"{_format_expr(expr.left)} {expr.operator} {_format_expr(expr.right)}"
219
+ elif isinstance(expr, nodes.PassExprNode):
220
+ return "pass"
221
+ return "?"
222
+
223
+
224
+ def _format_statement(stmt) -> str:
225
+ """Format a statement."""
226
+ from yuho.ast import nodes
227
+
228
+ if isinstance(stmt, nodes.ReturnStmt):
229
+ if stmt.value:
230
+ return f"return {_format_expr(stmt.value)};"
231
+ return "return;"
232
+ elif isinstance(stmt, nodes.PassStmt):
233
+ return "pass;"
234
+ elif isinstance(stmt, nodes.ExpressionStmt):
235
+ return f"{_format_expr(stmt.expression)};"
236
+ return "?"
237
+
238
+
239
+ def _format_duration(dur) -> str:
240
+ """Format a duration."""
241
+ parts = []
242
+ if dur.years:
243
+ parts.append(f"{dur.years} year{'s' if dur.years != 1 else ''}")
244
+ if dur.months:
245
+ parts.append(f"{dur.months} month{'s' if dur.months != 1 else ''}")
246
+ if dur.days:
247
+ parts.append(f"{dur.days} day{'s' if dur.days != 1 else ''}")
248
+ return ", ".join(parts) if parts else "0 days"
249
+
250
+
251
+ def _format_money(money) -> str:
252
+ """Format a money value."""
253
+ return f"${money.amount}"