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,410 @@
1
+ """
2
+ Graph command - visualize statute dependencies.
3
+
4
+ Generates dependency graphs in DOT or Mermaid format showing:
5
+ - Statute cross-references
6
+ - Import relationships
7
+ - Type dependencies
8
+ - Function call graphs
9
+ """
10
+
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Optional, List, Set, Dict, Tuple
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum, auto
16
+
17
+ import click
18
+
19
+ from yuho.parser import Parser
20
+ from yuho.ast import ASTBuilder
21
+ from yuho.ast.nodes import (
22
+ ModuleNode, StatuteNode, ImportNode, IdentifierNode,
23
+ FunctionCallNode, FieldAccessNode
24
+ )
25
+ from yuho.ast.visitor import Visitor
26
+ from yuho.cli.error_formatter import Colors, colorize
27
+
28
+
29
+ class GraphFormat(Enum):
30
+ """Supported graph output formats."""
31
+ DOT = "dot"
32
+ MERMAID = "mermaid"
33
+
34
+
35
+ @dataclass
36
+ class GraphNode:
37
+ """A node in the dependency graph."""
38
+ id: str
39
+ label: str
40
+ node_type: str # "statute", "import", "type", "function"
41
+
42
+ def __hash__(self):
43
+ return hash(self.id)
44
+
45
+ def __eq__(self, other):
46
+ return isinstance(other, GraphNode) and self.id == other.id
47
+
48
+
49
+ @dataclass
50
+ class GraphEdge:
51
+ """An edge in the dependency graph."""
52
+ source: str
53
+ target: str
54
+ edge_type: str # "references", "imports", "calls", "uses"
55
+ label: str = ""
56
+
57
+ def __hash__(self):
58
+ return hash((self.source, self.target, self.edge_type))
59
+
60
+ def __eq__(self, other):
61
+ return (isinstance(other, GraphEdge) and
62
+ self.source == other.source and
63
+ self.target == other.target and
64
+ self.edge_type == other.edge_type)
65
+
66
+
67
+ @dataclass
68
+ class DependencyGraph:
69
+ """Represents the full dependency graph."""
70
+ nodes: Set[GraphNode] = field(default_factory=set)
71
+ edges: Set[GraphEdge] = field(default_factory=set)
72
+
73
+ def add_node(self, node: GraphNode) -> None:
74
+ self.nodes.add(node)
75
+
76
+ def add_edge(self, edge: GraphEdge) -> None:
77
+ self.edges.add(edge)
78
+
79
+
80
+ class DependencyExtractor(Visitor):
81
+ """
82
+ Extract dependencies from a Yuho AST.
83
+
84
+ Identifies:
85
+ - Statute cross-references (via section numbers)
86
+ - Import statements
87
+ - Type usages
88
+ - Function calls
89
+ """
90
+
91
+ def __init__(self):
92
+ self.graph = DependencyGraph()
93
+ self._current_context: Optional[str] = None
94
+
95
+ def extract(self, ast: ModuleNode, filename: str = "main") -> DependencyGraph:
96
+ """Extract dependency graph from AST."""
97
+ self.graph = DependencyGraph()
98
+ self._current_context = None
99
+
100
+ # Add module node
101
+ module_id = f"module:{filename}"
102
+ self.graph.add_node(GraphNode(
103
+ id=module_id,
104
+ label=filename,
105
+ node_type="module"
106
+ ))
107
+
108
+ # Process imports
109
+ for imp in ast.imports:
110
+ self._process_import(module_id, imp)
111
+
112
+ # Process type definitions
113
+ for type_def in ast.type_defs:
114
+ type_id = f"type:{type_def.name}"
115
+ self.graph.add_node(GraphNode(
116
+ id=type_id,
117
+ label=type_def.name,
118
+ node_type="type"
119
+ ))
120
+ self.graph.add_edge(GraphEdge(
121
+ source=module_id,
122
+ target=type_id,
123
+ edge_type="defines"
124
+ ))
125
+
126
+ # Process functions
127
+ for func in ast.function_defs:
128
+ func_id = f"function:{func.name}"
129
+ self.graph.add_node(GraphNode(
130
+ id=func_id,
131
+ label=func.name,
132
+ node_type="function"
133
+ ))
134
+ self.graph.add_edge(GraphEdge(
135
+ source=module_id,
136
+ target=func_id,
137
+ edge_type="defines"
138
+ ))
139
+
140
+ # Process statutes
141
+ for statute in ast.statutes:
142
+ self._process_statute(module_id, statute)
143
+
144
+ return self.graph
145
+
146
+ def _process_import(self, module_id: str, imp: ImportNode) -> None:
147
+ """Process import statement."""
148
+ import_id = f"import:{imp.path}"
149
+ self.graph.add_node(GraphNode(
150
+ id=import_id,
151
+ label=imp.path,
152
+ node_type="import"
153
+ ))
154
+
155
+ if imp.is_wildcard:
156
+ label = "*"
157
+ elif imp.imported_names:
158
+ label = ", ".join(imp.imported_names[:3])
159
+ if len(imp.imported_names) > 3:
160
+ label += "..."
161
+ else:
162
+ label = ""
163
+
164
+ self.graph.add_edge(GraphEdge(
165
+ source=module_id,
166
+ target=import_id,
167
+ edge_type="imports",
168
+ label=label
169
+ ))
170
+
171
+ def _process_statute(self, module_id: str, statute: StatuteNode) -> None:
172
+ """Process statute node."""
173
+ statute_id = f"statute:{statute.section_number}"
174
+ title = statute.title.value if statute.title else f"Section {statute.section_number}"
175
+
176
+ self.graph.add_node(GraphNode(
177
+ id=statute_id,
178
+ label=f"S.{statute.section_number}\\n{title[:30]}",
179
+ node_type="statute"
180
+ ))
181
+
182
+ self.graph.add_edge(GraphEdge(
183
+ source=module_id,
184
+ target=statute_id,
185
+ edge_type="contains"
186
+ ))
187
+
188
+ # Look for cross-references in definitions and elements
189
+ self._current_context = statute_id
190
+
191
+ # Check definitions for section references
192
+ for defn in statute.definitions:
193
+ self._extract_section_refs(statute_id, defn.definition.value)
194
+
195
+ # Check illustrations
196
+ for illus in statute.illustrations:
197
+ self._extract_section_refs(statute_id, illus.description.value)
198
+
199
+ def _extract_section_refs(self, source_id: str, text: str) -> None:
200
+ """Extract section number references from text."""
201
+ import re
202
+
203
+ # Patterns for section references
204
+ patterns = [
205
+ r'[Ss]ection\s+(\d+[A-Za-z]*)',
206
+ r'[Ss]\.\s*(\d+[A-Za-z]*)',
207
+ r'§\s*(\d+[A-Za-z]*)',
208
+ ]
209
+
210
+ for pattern in patterns:
211
+ for match in re.finditer(pattern, text):
212
+ ref_section = match.group(1)
213
+ ref_id = f"statute:{ref_section}"
214
+
215
+ # Add target node if not exists
216
+ self.graph.add_node(GraphNode(
217
+ id=ref_id,
218
+ label=f"S.{ref_section}",
219
+ node_type="statute"
220
+ ))
221
+
222
+ # Add reference edge
223
+ self.graph.add_edge(GraphEdge(
224
+ source=source_id,
225
+ target=ref_id,
226
+ edge_type="references"
227
+ ))
228
+
229
+
230
+ def generate_dot(graph: DependencyGraph, title: str = "Yuho Dependencies") -> str:
231
+ """
232
+ Generate DOT format graph.
233
+
234
+ Args:
235
+ graph: The dependency graph
236
+ title: Graph title
237
+
238
+ Returns:
239
+ DOT format string
240
+ """
241
+ lines = [
242
+ f'digraph "{title}" {{',
243
+ ' rankdir=TB;',
244
+ ' node [fontname="Helvetica", fontsize=10];',
245
+ ' edge [fontname="Helvetica", fontsize=8];',
246
+ '',
247
+ ]
248
+
249
+ # Node styles by type
250
+ node_styles = {
251
+ "module": 'shape=box, style=filled, fillcolor="#E8F4F8"',
252
+ "statute": 'shape=box, style="filled,rounded", fillcolor="#FFF3CD"',
253
+ "import": 'shape=ellipse, style=filled, fillcolor="#D4EDDA"',
254
+ "type": 'shape=hexagon, style=filled, fillcolor="#F8D7DA"',
255
+ "function": 'shape=ellipse, style=filled, fillcolor="#CCE5FF"',
256
+ }
257
+
258
+ # Add nodes
259
+ for node in sorted(graph.nodes, key=lambda n: n.id):
260
+ style = node_styles.get(node.node_type, '')
261
+ safe_label = node.label.replace('"', '\\"')
262
+ lines.append(f' "{node.id}" [label="{safe_label}", {style}];')
263
+
264
+ lines.append('')
265
+
266
+ # Edge styles by type
267
+ edge_styles = {
268
+ "imports": 'style=dashed, color="#28a745"',
269
+ "references": 'style=solid, color="#dc3545"',
270
+ "defines": 'style=solid, color="#6c757d"',
271
+ "contains": 'style=dotted, color="#6c757d"',
272
+ "calls": 'style=solid, color="#007bff"',
273
+ "uses": 'style=dashed, color="#17a2b8"',
274
+ }
275
+
276
+ # Add edges
277
+ for edge in sorted(graph.edges, key=lambda e: (e.source, e.target)):
278
+ style = edge_styles.get(edge.edge_type, '')
279
+ label_part = f', label="{edge.label}"' if edge.label else ''
280
+ lines.append(f' "{edge.source}" -> "{edge.target}" [{style}{label_part}];')
281
+
282
+ lines.append('}')
283
+ return '\n'.join(lines)
284
+
285
+
286
+ def generate_mermaid(graph: DependencyGraph, title: str = "Yuho Dependencies") -> str:
287
+ """
288
+ Generate Mermaid format graph.
289
+
290
+ Args:
291
+ graph: The dependency graph
292
+ title: Graph title
293
+
294
+ Returns:
295
+ Mermaid format string
296
+ """
297
+ lines = [
298
+ '```mermaid',
299
+ 'flowchart TB',
300
+ f' %% {title}',
301
+ '',
302
+ ]
303
+
304
+ # Node shapes by type
305
+ def node_shape(node: GraphNode) -> str:
306
+ safe_label = node.label.replace('"', "'").replace('\n', ' ')
307
+ shapes = {
308
+ "module": f'["{safe_label}"]',
309
+ "statute": f'("{safe_label}")',
310
+ "import": f'[["{safe_label}"]]',
311
+ "type": f'{{{{{safe_label}}}}}',
312
+ "function": f'(["{safe_label}"])',
313
+ }
314
+ return shapes.get(node.node_type, f'["{safe_label}"]')
315
+
316
+ # Sanitize ID for Mermaid
317
+ def safe_id(id: str) -> str:
318
+ return id.replace(":", "_").replace(".", "_").replace("/", "_")
319
+
320
+ # Add nodes
321
+ for node in sorted(graph.nodes, key=lambda n: n.id):
322
+ sid = safe_id(node.id)
323
+ lines.append(f' {sid}{node_shape(node)}')
324
+
325
+ lines.append('')
326
+
327
+ # Edge styles
328
+ edge_arrows = {
329
+ "imports": "-.->",
330
+ "references": "-->",
331
+ "defines": "-->",
332
+ "contains": "-.->",
333
+ "calls": "-->",
334
+ "uses": "-.->",
335
+ }
336
+
337
+ # Add edges
338
+ for edge in sorted(graph.edges, key=lambda e: (e.source, e.target)):
339
+ src = safe_id(edge.source)
340
+ tgt = safe_id(edge.target)
341
+ arrow = edge_arrows.get(edge.edge_type, "-->")
342
+ if edge.label:
343
+ lines.append(f' {src} {arrow}|{edge.label}| {tgt}')
344
+ else:
345
+ lines.append(f' {src} {arrow} {tgt}')
346
+
347
+ lines.append('```')
348
+ return '\n'.join(lines)
349
+
350
+
351
+ def run_graph(
352
+ file: str,
353
+ format: str = "mermaid",
354
+ output: Optional[str] = None,
355
+ verbose: bool = False,
356
+ color: bool = True,
357
+ ) -> None:
358
+ """
359
+ Generate dependency graph for a Yuho file.
360
+
361
+ Args:
362
+ file: Path to the .yh file
363
+ format: Output format (dot or mermaid)
364
+ output: Output file path (stdout if None)
365
+ verbose: Enable verbose output
366
+ color: Use colored output
367
+ """
368
+ path = Path(file)
369
+
370
+ if not path.exists():
371
+ click.echo(colorize(f"error: File not found: {file}", Colors.RED), err=True)
372
+ sys.exit(1)
373
+
374
+ # Parse file
375
+ parser = Parser()
376
+ result = parser.parse_file(path)
377
+
378
+ if result.errors:
379
+ click.echo(colorize(f"error: Parse errors in {file}:", Colors.RED), err=True)
380
+ for err in result.errors[:3]:
381
+ click.echo(f" {err.message}", err=True)
382
+ sys.exit(1)
383
+
384
+ # Build AST
385
+ builder = ASTBuilder()
386
+ ast = builder.build(result.tree)
387
+
388
+ # Extract dependencies
389
+ extractor = DependencyExtractor()
390
+ graph = extractor.extract(ast, path.stem)
391
+
392
+ if verbose:
393
+ click.echo(f"Extracted {len(graph.nodes)} nodes and {len(graph.edges)} edges")
394
+
395
+ # Generate output
396
+ graph_format = GraphFormat(format.lower())
397
+
398
+ if graph_format == GraphFormat.DOT:
399
+ output_str = generate_dot(graph, title=f"Dependencies: {path.name}")
400
+ else:
401
+ output_str = generate_mermaid(graph, title=f"Dependencies: {path.name}")
402
+
403
+ # Write output
404
+ if output:
405
+ out_path = Path(output)
406
+ out_path.write_text(output_str)
407
+ if verbose:
408
+ click.echo(f"Wrote graph to {out_path}")
409
+ else:
410
+ print(output_str)
@@ -0,0 +1,120 @@
1
+ """
2
+ Init command - scaffold new Yuho project.
3
+ """
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import click
10
+
11
+ from yuho.cli.error_formatter import Colors, colorize
12
+
13
+
14
+ TEMPLATE_STATUTE = '''// {name} - Yuho statute definition
15
+ // Section: S{section}
16
+
17
+ statute {section} "{name}" {{
18
+ definitions {{
19
+ // Define key terms here
20
+ // term := "definition";
21
+ }}
22
+
23
+ elements {{
24
+ // Define the elements of the offense
25
+ // actus_reus physical_act := "description";
26
+ // mens_rea mental_state := "description";
27
+ }}
28
+
29
+ penalty {{
30
+ // imprisonment := 0 years .. 1 year;
31
+ // fine := $0 .. $10,000;
32
+ }}
33
+
34
+ illustration A {{
35
+ "Example scenario illustrating the statute."
36
+ }}
37
+ }}
38
+ '''
39
+
40
+ TEMPLATE_METADATA = '''# Metadata for {name}
41
+
42
+ [statute]
43
+ section_number = "{section}"
44
+ title = "{name}"
45
+ jurisdiction = "SG" # Singapore
46
+
47
+ [contributor]
48
+ name = ""
49
+ email = ""
50
+
51
+ [version]
52
+ current = "1.0.0"
53
+ '''
54
+
55
+ TEMPLATE_TEST = '''// Tests for {name}
56
+ // Run with: yuho test {filename}
57
+
58
+ // Test case 1: Basic scenario
59
+ // TODO: Add test cases
60
+ '''
61
+
62
+
63
+ def run_init(name: Optional[str] = None, directory: Optional[str] = None, verbose: bool = False) -> None:
64
+ """
65
+ Initialize a new Yuho statute project.
66
+
67
+ Args:
68
+ name: Project name
69
+ directory: Directory to create project in
70
+ verbose: Enable verbose output
71
+ """
72
+ # Get project name
73
+ if not name:
74
+ name = click.prompt("Statute name", default="MyStatute")
75
+
76
+ # Clean name for filenames
77
+ safe_name = "".join(c if c.isalnum() else "_" for c in name)
78
+ section = click.prompt("Section number", default="000")
79
+
80
+ # Determine directory
81
+ if directory:
82
+ project_dir = Path(directory)
83
+ else:
84
+ project_dir = Path.cwd() / safe_name.lower()
85
+
86
+ # Check if exists
87
+ if project_dir.exists():
88
+ if not click.confirm(f"Directory {project_dir} exists. Continue?"):
89
+ sys.exit(0)
90
+ else:
91
+ project_dir.mkdir(parents=True)
92
+
93
+ # Create files
94
+ statute_file = project_dir / f"{safe_name.lower()}.yh"
95
+ metadata_file = project_dir / "metadata.toml"
96
+ test_dir = project_dir / "tests"
97
+ test_file = test_dir / f"test_{safe_name.lower()}.yh"
98
+
99
+ # Write statute template
100
+ statute_content = TEMPLATE_STATUTE.format(name=name, section=section)
101
+ statute_file.write_text(statute_content, encoding="utf-8")
102
+
103
+ # Write metadata template
104
+ metadata_content = TEMPLATE_METADATA.format(name=name, section=section)
105
+ metadata_file.write_text(metadata_content, encoding="utf-8")
106
+
107
+ # Write test template
108
+ test_dir.mkdir(exist_ok=True)
109
+ test_content = TEMPLATE_TEST.format(name=name, filename=statute_file.name)
110
+ test_file.write_text(test_content, encoding="utf-8")
111
+
112
+ click.echo(colorize(f"Created Yuho project: {project_dir}", Colors.CYAN + Colors.BOLD))
113
+ click.echo(f" {statute_file.name}")
114
+ click.echo(f" metadata.toml")
115
+ click.echo(f" tests/test_{safe_name.lower()}.yh")
116
+ click.echo()
117
+ click.echo("Next steps:")
118
+ click.echo(f" 1. Edit {statute_file.name} to define your statute")
119
+ click.echo(f" 2. Run 'yuho check {statute_file}' to validate")
120
+ click.echo(f" 3. Run 'yuho transpile {statute_file} -t english' to see English version")