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,470 @@
1
+ """
2
+ English transpiler - controlled natural language generation.
3
+
4
+ Converts Yuho AST to readable English text using templates
5
+ for legal documents.
6
+ """
7
+
8
+ from typing import List, Optional
9
+
10
+ from yuho.ast import nodes
11
+ from yuho.ast.visitor import Visitor
12
+ from yuho.transpile.base import TranspileTarget, TranspilerBase
13
+
14
+
15
+ class EnglishTranspiler(TranspilerBase, Visitor):
16
+ """
17
+ Transpile Yuho AST to controlled natural language (English).
18
+
19
+ Uses templates for legal text generation with proper clause
20
+ connectors and penalty formatting.
21
+ """
22
+
23
+ def __init__(self):
24
+ self._output: List[str] = []
25
+ self._indent_level = 0
26
+
27
+ @property
28
+ def target(self) -> TranspileTarget:
29
+ return TranspileTarget.ENGLISH
30
+
31
+ def transpile(self, ast: nodes.ModuleNode) -> str:
32
+ """Transpile AST to English text."""
33
+ self._output = []
34
+ self._indent_level = 0
35
+ self._visit_module(ast)
36
+ return "\n".join(self._output)
37
+
38
+ def _emit(self, text: str) -> None:
39
+ """Add a line to output with current indentation."""
40
+ indent = " " * self._indent_level
41
+ self._output.append(f"{indent}{text}")
42
+
43
+ def _emit_blank(self) -> None:
44
+ """Add a blank line."""
45
+ self._output.append("")
46
+
47
+ # =========================================================================
48
+ # Module and imports
49
+ # =========================================================================
50
+
51
+ def _visit_module(self, node: nodes.ModuleNode) -> None:
52
+ """Generate English for entire module."""
53
+ # Imports
54
+ if node.imports:
55
+ for imp in node.imports:
56
+ self._visit_import(imp)
57
+ self._emit_blank()
58
+
59
+ # Type definitions
60
+ if node.type_defs:
61
+ self._emit("TYPE DEFINITIONS")
62
+ self._emit("=" * 50)
63
+ self._emit_blank()
64
+ for struct in node.type_defs:
65
+ self._visit_struct_def(struct)
66
+ self._emit_blank()
67
+
68
+ # Functions
69
+ if node.function_defs:
70
+ self._emit("FUNCTIONS")
71
+ self._emit("=" * 50)
72
+ self._emit_blank()
73
+ for func in node.function_defs:
74
+ self._visit_function_def(func)
75
+ self._emit_blank()
76
+
77
+ # Statutes (main content)
78
+ for statute in node.statutes:
79
+ self._visit_statute(statute)
80
+ self._emit_blank()
81
+
82
+ # Variables
83
+ if node.variables:
84
+ self._emit("DECLARATIONS")
85
+ self._emit("=" * 50)
86
+ self._emit_blank()
87
+ for var in node.variables:
88
+ self._visit_variable_decl(var)
89
+
90
+ def _visit_import(self, node: nodes.ImportNode) -> None:
91
+ """Generate English for import."""
92
+ if node.is_wildcard:
93
+ self._emit(f"Reference: All definitions from \"{node.path}\"")
94
+ elif node.imported_names:
95
+ names = ", ".join(node.imported_names)
96
+ self._emit(f"Reference: {names} from \"{node.path}\"")
97
+ else:
98
+ self._emit(f"Reference: \"{node.path}\"")
99
+
100
+ # =========================================================================
101
+ # Statute blocks
102
+ # =========================================================================
103
+
104
+ def _visit_statute(self, node: nodes.StatuteNode) -> None:
105
+ """Generate English for statute."""
106
+ # Header
107
+ title = node.title.value if node.title else "Untitled"
108
+ self._emit(f"SECTION {node.section_number}: {title}")
109
+ self._emit("=" * 60)
110
+ self._emit_blank()
111
+
112
+ # Definitions
113
+ if node.definitions:
114
+ self._emit("Definitions:")
115
+ self._indent_level += 1
116
+ for defn in node.definitions:
117
+ self._emit(f'"{defn.term}" means {defn.definition.value}')
118
+ self._indent_level -= 1
119
+ self._emit_blank()
120
+
121
+ # Elements
122
+ if node.elements:
123
+ self._emit("Elements of the offence:")
124
+ self._indent_level += 1
125
+ for elem in node.elements:
126
+ self._visit_element(elem)
127
+ self._indent_level -= 1
128
+ self._emit_blank()
129
+
130
+ # Penalty
131
+ if node.penalty:
132
+ self._visit_penalty(node.penalty)
133
+ self._emit_blank()
134
+
135
+ # Illustrations
136
+ if node.illustrations:
137
+ self._emit("Illustrations:")
138
+ self._emit_blank()
139
+ for i, illus in enumerate(node.illustrations, 1):
140
+ label = illus.label or f"({chr(ord('a') + i - 1)})"
141
+ self._emit(f"{label} {illus.description.value}")
142
+ self._emit_blank()
143
+
144
+ def _visit_element(self, node: nodes.ElementNode) -> None:
145
+ """Generate English for element."""
146
+ type_labels = {
147
+ "actus_reus": "Physical element (actus reus)",
148
+ "mens_rea": "Mental element (mens rea)",
149
+ "circumstance": "Circumstance",
150
+ }
151
+ label = type_labels.get(node.element_type, node.element_type)
152
+
153
+ # Handle description
154
+ if isinstance(node.description, nodes.StringLit):
155
+ self._emit(f"{label}: {node.description.value}")
156
+ elif isinstance(node.description, nodes.MatchExprNode):
157
+ self._emit(f"{label}: {node.name}")
158
+ self._indent_level += 1
159
+ self._visit_match_expr_english(node.description)
160
+ self._indent_level -= 1
161
+ else:
162
+ desc = self._expr_to_english(node.description)
163
+ self._emit(f"{label}: {desc}")
164
+
165
+ def _visit_penalty(self, node: nodes.PenaltyNode) -> None:
166
+ """Generate English for penalty."""
167
+ self._emit("Penalty:")
168
+ self._indent_level += 1
169
+
170
+ parts: List[str] = []
171
+
172
+ # Imprisonment
173
+ if node.imprisonment_max:
174
+ if node.imprisonment_min:
175
+ min_str = self._duration_to_english(node.imprisonment_min)
176
+ max_str = self._duration_to_english(node.imprisonment_max)
177
+ parts.append(f"imprisonment for a term of not less than {min_str} and not more than {max_str}")
178
+ else:
179
+ max_str = self._duration_to_english(node.imprisonment_max)
180
+ parts.append(f"imprisonment for a term which may extend to {max_str}")
181
+
182
+ # Fine
183
+ if node.fine_max:
184
+ if node.fine_min:
185
+ min_str = self._money_to_english(node.fine_min)
186
+ max_str = self._money_to_english(node.fine_max)
187
+ parts.append(f"a fine of not less than {min_str} and not more than {max_str}")
188
+ else:
189
+ max_str = self._money_to_english(node.fine_max)
190
+ parts.append(f"a fine which may extend to {max_str}")
191
+
192
+ # Combine parts
193
+ if len(parts) == 2:
194
+ self._emit(f"Shall be punished with {parts[0]}, or with {parts[1]}, or with both.")
195
+ elif len(parts) == 1:
196
+ self._emit(f"Shall be punished with {parts[0]}.")
197
+ else:
198
+ self._emit("Penalty to be determined.")
199
+
200
+ # Supplementary
201
+ if node.supplementary:
202
+ self._emit_blank()
203
+ self._emit(f"Additionally: {node.supplementary.value}")
204
+
205
+ self._indent_level -= 1
206
+
207
+ # =========================================================================
208
+ # Match expressions
209
+ # =========================================================================
210
+
211
+ def _visit_match_expr_english(self, node: nodes.MatchExprNode) -> None:
212
+ """Generate English for match expression."""
213
+ if node.scrutinee:
214
+ scrutinee = self._expr_to_english(node.scrutinee)
215
+ self._emit(f"Based on {scrutinee}:")
216
+
217
+ for i, arm in enumerate(node.arms):
218
+ self._visit_match_arm_english(arm, i, len(node.arms))
219
+
220
+ def _visit_match_arm_english(self, node: nodes.MatchArm, index: int, total: int) -> None:
221
+ """Generate English for match arm."""
222
+ # Pattern
223
+ pattern = self._pattern_to_english(node.pattern)
224
+
225
+ # Guard
226
+ guard_str = ""
227
+ if node.guard:
228
+ guard = self._expr_to_english(node.guard)
229
+ guard_str = f", provided that {guard}"
230
+
231
+ # Body
232
+ body = self._expr_to_english(node.body)
233
+
234
+ # Connectors based on position
235
+ if index == 0:
236
+ connector = "If"
237
+ elif isinstance(node.pattern, nodes.WildcardPattern):
238
+ connector = "Otherwise"
239
+ pattern = ""
240
+ else:
241
+ connector = "If"
242
+
243
+ if pattern:
244
+ self._emit(f"{connector} {pattern}{guard_str}: {body}")
245
+ else:
246
+ self._emit(f"{connector}{guard_str}: {body}")
247
+
248
+ # =========================================================================
249
+ # Struct definitions
250
+ # =========================================================================
251
+
252
+ def _visit_struct_def(self, node: nodes.StructDefNode) -> None:
253
+ """Generate English for struct definition."""
254
+ self._emit(f"Type \"{node.name}\" consists of:")
255
+ self._indent_level += 1
256
+ for field in node.fields:
257
+ type_str = self._type_to_english(field.type_annotation)
258
+ self._emit(f"- {field.name}: {type_str}")
259
+ self._indent_level -= 1
260
+
261
+ # =========================================================================
262
+ # Function definitions
263
+ # =========================================================================
264
+
265
+ def _visit_function_def(self, node: nodes.FunctionDefNode) -> None:
266
+ """Generate English for function definition."""
267
+ params_str = ", ".join(
268
+ f"{p.name} ({self._type_to_english(p.type_annotation)})"
269
+ for p in node.params
270
+ )
271
+ ret_str = f" returning {self._type_to_english(node.return_type)}" if node.return_type else ""
272
+
273
+ self._emit(f"Function \"{node.name}\"({params_str}){ret_str}:")
274
+ self._indent_level += 1
275
+ for stmt in node.body.statements:
276
+ self._visit_statement(stmt)
277
+ self._indent_level -= 1
278
+
279
+ # =========================================================================
280
+ # Statements
281
+ # =========================================================================
282
+
283
+ def _visit_variable_decl(self, node: nodes.VariableDecl) -> None:
284
+ """Generate English for variable declaration."""
285
+ type_str = self._type_to_english(node.type_annotation)
286
+ if node.value:
287
+ value_str = self._expr_to_english(node.value)
288
+ self._emit(f"Let {node.name} be a {type_str} with value {value_str}.")
289
+ else:
290
+ self._emit(f"Let {node.name} be a {type_str}.")
291
+
292
+ def _visit_statement(self, node: nodes.ASTNode) -> None:
293
+ """Generate English for statement."""
294
+ if isinstance(node, nodes.VariableDecl):
295
+ self._visit_variable_decl(node)
296
+ elif isinstance(node, nodes.AssignmentStmt):
297
+ target = self._expr_to_english(node.target)
298
+ value = self._expr_to_english(node.value)
299
+ self._emit(f"Set {target} to {value}.")
300
+ elif isinstance(node, nodes.ReturnStmt):
301
+ if node.value:
302
+ value = self._expr_to_english(node.value)
303
+ self._emit(f"Return {value}.")
304
+ else:
305
+ self._emit("Return.")
306
+ elif isinstance(node, nodes.PassStmt):
307
+ self._emit("(No action)")
308
+ elif isinstance(node, nodes.ExpressionStmt):
309
+ expr = self._expr_to_english(node.expression)
310
+ self._emit(f"{expr}")
311
+
312
+ # =========================================================================
313
+ # Helper methods for English generation
314
+ # =========================================================================
315
+
316
+ def _expr_to_english(self, node: nodes.ASTNode) -> str:
317
+ """Convert expression to English."""
318
+ if isinstance(node, nodes.IntLit):
319
+ return str(node.value)
320
+ elif isinstance(node, nodes.FloatLit):
321
+ return str(node.value)
322
+ elif isinstance(node, nodes.BoolLit):
323
+ return "true" if node.value else "false"
324
+ elif isinstance(node, nodes.StringLit):
325
+ return f'"{node.value}"'
326
+ elif isinstance(node, nodes.MoneyNode):
327
+ return self._money_to_english(node)
328
+ elif isinstance(node, nodes.PercentNode):
329
+ return f"{node.value}%"
330
+ elif isinstance(node, nodes.DateNode):
331
+ return node.value.strftime("%d %B %Y")
332
+ elif isinstance(node, nodes.DurationNode):
333
+ return self._duration_to_english(node)
334
+ elif isinstance(node, nodes.IdentifierNode):
335
+ return node.name
336
+ elif isinstance(node, nodes.FieldAccessNode):
337
+ base = self._expr_to_english(node.base)
338
+ return f"{base}'s {node.field_name}"
339
+ elif isinstance(node, nodes.IndexAccessNode):
340
+ base = self._expr_to_english(node.base)
341
+ index = self._expr_to_english(node.index)
342
+ return f"{base}[{index}]"
343
+ elif isinstance(node, nodes.FunctionCallNode):
344
+ callee = self._expr_to_english(node.callee)
345
+ args = ", ".join(self._expr_to_english(a) for a in node.args)
346
+ return f"{callee}({args})"
347
+ elif isinstance(node, nodes.BinaryExprNode):
348
+ left = self._expr_to_english(node.left)
349
+ right = self._expr_to_english(node.right)
350
+ op = self._operator_to_english(node.operator)
351
+ return f"{left} {op} {right}"
352
+ elif isinstance(node, nodes.UnaryExprNode):
353
+ operand = self._expr_to_english(node.operand)
354
+ if node.operator == "!":
355
+ return f"not {operand}"
356
+ return f"{node.operator}{operand}"
357
+ elif isinstance(node, nodes.PassExprNode):
358
+ return "nothing"
359
+ elif isinstance(node, nodes.MatchExprNode):
360
+ return "(see decision tree below)"
361
+ elif isinstance(node, nodes.StructLiteralNode):
362
+ if node.struct_name:
363
+ return f"a {node.struct_name}"
364
+ return "a record"
365
+ else:
366
+ return str(node)
367
+
368
+ def _pattern_to_english(self, node: nodes.PatternNode) -> str:
369
+ """Convert pattern to English."""
370
+ if isinstance(node, nodes.WildcardPattern):
371
+ return "in any other case"
372
+ elif isinstance(node, nodes.LiteralPattern):
373
+ return f"the value is {self._expr_to_english(node.literal)}"
374
+ elif isinstance(node, nodes.BindingPattern):
375
+ return f"the value (let us call it {node.name})"
376
+ elif isinstance(node, nodes.StructPattern):
377
+ fields = ", ".join(fp.name for fp in node.fields)
378
+ return f"it matches {node.type_name} with {fields}"
379
+ else:
380
+ return "the condition is met"
381
+
382
+ def _type_to_english(self, node: nodes.TypeNode) -> str:
383
+ """Convert type to English."""
384
+ if isinstance(node, nodes.BuiltinType):
385
+ type_names = {
386
+ "int": "integer",
387
+ "float": "decimal number",
388
+ "bool": "boolean",
389
+ "string": "text",
390
+ "money": "monetary amount",
391
+ "percent": "percentage",
392
+ "date": "date",
393
+ "duration": "time period",
394
+ "void": "nothing",
395
+ }
396
+ return type_names.get(node.name, node.name)
397
+ elif isinstance(node, nodes.NamedType):
398
+ return node.name
399
+ elif isinstance(node, nodes.OptionalType):
400
+ inner = self._type_to_english(node.inner)
401
+ return f"optional {inner}"
402
+ elif isinstance(node, nodes.ArrayType):
403
+ elem = self._type_to_english(node.element_type)
404
+ return f"list of {elem}"
405
+ elif isinstance(node, nodes.GenericType):
406
+ args = ", ".join(self._type_to_english(a) for a in node.type_args)
407
+ return f"{node.base} of {args}"
408
+ else:
409
+ return "value"
410
+
411
+ def _operator_to_english(self, op: str) -> str:
412
+ """Convert operator to English."""
413
+ operators = {
414
+ "+": "plus",
415
+ "-": "minus",
416
+ "*": "times",
417
+ "/": "divided by",
418
+ "%": "modulo",
419
+ "==": "equals",
420
+ "!=": "does not equal",
421
+ "<": "is less than",
422
+ ">": "is greater than",
423
+ "<=": "is at most",
424
+ ">=": "is at least",
425
+ "&&": "and",
426
+ "||": "or",
427
+ }
428
+ return operators.get(op, op)
429
+
430
+ def _duration_to_english(self, node: nodes.DurationNode) -> str:
431
+ """Convert duration to English."""
432
+ parts: List[str] = []
433
+ if node.years:
434
+ parts.append(f"{node.years} year{'s' if node.years != 1 else ''}")
435
+ if node.months:
436
+ parts.append(f"{node.months} month{'s' if node.months != 1 else ''}")
437
+ if node.days:
438
+ parts.append(f"{node.days} day{'s' if node.days != 1 else ''}")
439
+ if node.hours:
440
+ parts.append(f"{node.hours} hour{'s' if node.hours != 1 else ''}")
441
+ if node.minutes:
442
+ parts.append(f"{node.minutes} minute{'s' if node.minutes != 1 else ''}")
443
+ if node.seconds:
444
+ parts.append(f"{node.seconds} second{'s' if node.seconds != 1 else ''}")
445
+
446
+ if not parts:
447
+ return "no time"
448
+ elif len(parts) == 1:
449
+ return parts[0]
450
+ else:
451
+ return ", ".join(parts[:-1]) + " and " + parts[-1]
452
+
453
+ def _money_to_english(self, node: nodes.MoneyNode) -> str:
454
+ """Convert money to English."""
455
+ currency_symbols = {
456
+ nodes.Currency.SGD: "S$",
457
+ nodes.Currency.USD: "US$",
458
+ nodes.Currency.EUR: "€",
459
+ nodes.Currency.GBP: "£",
460
+ nodes.Currency.JPY: "¥",
461
+ nodes.Currency.CNY: "¥",
462
+ nodes.Currency.INR: "₹",
463
+ nodes.Currency.AUD: "A$",
464
+ nodes.Currency.CAD: "C$",
465
+ nodes.Currency.CHF: "CHF ",
466
+ }
467
+ symbol = currency_symbols.get(node.currency, "$")
468
+ # Format with thousands separator
469
+ amount_str = f"{node.amount:,.2f}"
470
+ return f"{symbol}{amount_str}"