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
yuho/verify/alloy.py ADDED
@@ -0,0 +1,352 @@
1
+ """
2
+ Alloy model generation and analysis for Yuho statutes.
3
+
4
+ Generates Alloy models from statute ASTs for bounded model checking,
5
+ invokes the Alloy analyzer, and parses counterexamples.
6
+ """
7
+
8
+ from typing import List, Dict, Any, Optional, Tuple
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ import subprocess
12
+ import tempfile
13
+ import json
14
+ import re
15
+ import logging
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class AlloyCounterexample:
22
+ """A counterexample from Alloy analysis."""
23
+ assertion_name: str
24
+ violated: bool
25
+ witness: Dict[str, Any] = field(default_factory=dict)
26
+ message: str = ""
27
+
28
+ def to_diagnostic(self) -> Dict[str, Any]:
29
+ """Convert to LSP-compatible diagnostic."""
30
+ return {
31
+ "message": f"Alloy: {self.assertion_name} - {self.message}",
32
+ "severity": "warning" if self.violated else "info",
33
+ "source": "alloy",
34
+ "data": self.witness,
35
+ }
36
+
37
+
38
+ class AlloyGenerator:
39
+ """
40
+ Generates Alloy models from Yuho statute ASTs.
41
+
42
+ Translates statute elements, penalties, and constraints into
43
+ Alloy's relational modeling language for bounded verification.
44
+ """
45
+
46
+ def __init__(self, scope: int = 5):
47
+ """
48
+ Initialize the generator.
49
+
50
+ Args:
51
+ scope: Default scope for bounded model checking (max instances)
52
+ """
53
+ self.scope = scope
54
+
55
+ def generate(self, ast) -> str:
56
+ """
57
+ Generate Alloy model from a module AST.
58
+
59
+ Args:
60
+ ast: ModuleNode from Yuho AST
61
+
62
+ Returns:
63
+ Alloy model as string
64
+ """
65
+ lines = [
66
+ "// Generated Alloy model from Yuho statute",
67
+ "// Run assertions to verify statute consistency",
68
+ "",
69
+ "module yuho_statute",
70
+ "",
71
+ ]
72
+
73
+ # Generate signatures for types
74
+ lines.extend(self._generate_signatures(ast))
75
+
76
+ # Generate facts from statutes
77
+ lines.extend(self._generate_facts(ast))
78
+
79
+ # Generate assertions
80
+ lines.extend(self._generate_assertions(ast))
81
+
82
+ # Generate run/check commands
83
+ lines.extend(self._generate_commands())
84
+
85
+ return "\n".join(lines)
86
+
87
+ def _generate_signatures(self, ast) -> List[str]:
88
+ """Generate Alloy signatures from type definitions."""
89
+ lines = [
90
+ "// Base types",
91
+ "abstract sig Person {}",
92
+ "sig Defendant extends Person {}",
93
+ "sig Victim extends Person {}",
94
+ "",
95
+ "abstract sig Intent {}",
96
+ "one sig Intentional, Reckless, Negligent extends Intent {}",
97
+ "",
98
+ "abstract sig Element {",
99
+ " satisfied: one Bool",
100
+ "}",
101
+ "",
102
+ "abstract sig Bool {}",
103
+ "one sig True, False extends Bool {}",
104
+ "",
105
+ ]
106
+
107
+ # Generate from struct definitions
108
+ for struct in ast.type_defs:
109
+ sig_lines = [f"sig {struct.name} {{"]
110
+ for field_name, field_type in struct.fields.items():
111
+ alloy_type = self._type_to_alloy(field_type)
112
+ sig_lines.append(f" {field_name}: {alloy_type},")
113
+ if sig_lines[-1].endswith(","):
114
+ sig_lines[-1] = sig_lines[-1][:-1] # Remove trailing comma
115
+ sig_lines.append("}")
116
+ lines.extend(sig_lines)
117
+ lines.append("")
118
+
119
+ return lines
120
+
121
+ def _generate_facts(self, ast) -> List[str]:
122
+ """Generate Alloy facts from statutes."""
123
+ lines = ["// Facts derived from statute elements", ""]
124
+
125
+ for statute in ast.statutes:
126
+ lines.append(f"// Statute {statute.section_number}")
127
+
128
+ # Generate signature for this statute
129
+ statute_name = f"Statute_{statute.section_number.replace('.', '_')}"
130
+ lines.append(f"one sig {statute_name} {{")
131
+
132
+ # Add element fields
133
+ if statute.elements:
134
+ element_names = []
135
+ for elem in statute.elements.elements:
136
+ elem_name = elem.name.replace(" ", "_")
137
+ element_names.append(elem_name)
138
+ lines.append(f" {elem_name}: one Element,")
139
+
140
+ if lines[-1].endswith(","):
141
+ lines[-1] = lines[-1][:-1]
142
+
143
+ lines.append("}")
144
+ lines.append("")
145
+
146
+ # Generate fact for element relationships
147
+ if statute.elements:
148
+ lines.append(f"fact {statute_name}_elements {{")
149
+ lines.append(f" // All elements must be satisfied for conviction")
150
+
151
+ elem_refs = [f"{statute_name}.{e.name.replace(' ', '_')}.satisfied = True"
152
+ for e in statute.elements.elements]
153
+ if elem_refs:
154
+ lines.append(f" // {' and '.join(elem_refs)}")
155
+
156
+ lines.append("}")
157
+ lines.append("")
158
+
159
+ return lines
160
+
161
+ def _generate_assertions(self, ast) -> List[str]:
162
+ """Generate Alloy assertions for verification."""
163
+ lines = [
164
+ "// Assertions",
165
+ "",
166
+ "// No contradictory elements",
167
+ "assert no_contradictory_elements {",
168
+ " // No element can be both satisfied and not satisfied",
169
+ " all e: Element | e.satisfied = True or e.satisfied = False",
170
+ "}",
171
+ "",
172
+ "// Penalty ordering: more serious crimes should have higher penalties",
173
+ "assert penalty_ordering {",
174
+ " // Placeholder: define penalty comparison logic",
175
+ " // In a complete implementation, compare penalty maximums",
176
+ "}",
177
+ "",
178
+ ]
179
+
180
+ # Add statute-specific assertions
181
+ for statute in ast.statutes:
182
+ statute_name = f"Statute_{statute.section_number.replace('.', '_')}"
183
+
184
+ lines.append(f"// Assertions for {statute.section_number}")
185
+ lines.append(f"assert {statute_name}_consistent {{")
186
+ lines.append(f" // Statute elements are internally consistent")
187
+ lines.append("}")
188
+ lines.append("")
189
+
190
+ return lines
191
+
192
+ def _generate_commands(self) -> List[str]:
193
+ """Generate Alloy run/check commands."""
194
+ return [
195
+ "// Verification commands",
196
+ f"check no_contradictory_elements for {self.scope}",
197
+ f"check penalty_ordering for {self.scope}",
198
+ "",
199
+ "// Run command for exploration",
200
+ f"run show_model for {self.scope}",
201
+ ]
202
+
203
+ def _type_to_alloy(self, yuho_type: str) -> str:
204
+ """Convert Yuho type to Alloy type."""
205
+ type_map = {
206
+ "int": "Int",
207
+ "bool": "Bool",
208
+ "string": "String",
209
+ "money": "Int", # Represent as cents
210
+ "percent": "Int",
211
+ "date": "Int", # Unix timestamp
212
+ "duration": "Int", # Days
213
+ }
214
+ return type_map.get(yuho_type, yuho_type)
215
+
216
+
217
+ class AlloyAnalyzer:
218
+ """
219
+ Invokes the Alloy analyzer and parses results.
220
+
221
+ Requires Alloy to be installed and accessible.
222
+ """
223
+
224
+ def __init__(
225
+ self,
226
+ alloy_jar: Optional[str] = None,
227
+ timeout: int = 30,
228
+ ):
229
+ """
230
+ Initialize the analyzer.
231
+
232
+ Args:
233
+ alloy_jar: Path to Alloy JAR file (auto-detect if None)
234
+ timeout: Timeout in seconds for analysis
235
+ """
236
+ self.alloy_jar = alloy_jar or self._find_alloy_jar()
237
+ self.timeout = timeout
238
+
239
+ def _find_alloy_jar(self) -> Optional[str]:
240
+ """Try to find Alloy JAR in common locations."""
241
+ common_paths = [
242
+ Path.home() / ".alloy" / "alloy.jar",
243
+ Path("/usr/local/share/alloy/alloy.jar"),
244
+ Path("/opt/alloy/alloy.jar"),
245
+ ]
246
+
247
+ for path in common_paths:
248
+ if path.exists():
249
+ return str(path)
250
+
251
+ return None
252
+
253
+ def is_available(self) -> bool:
254
+ """Check if Alloy analyzer is available."""
255
+ if not self.alloy_jar:
256
+ return False
257
+ return Path(self.alloy_jar).exists()
258
+
259
+ def analyze(self, model: str) -> List[AlloyCounterexample]:
260
+ """
261
+ Run Alloy analyzer on a model.
262
+
263
+ Args:
264
+ model: Alloy model as string
265
+
266
+ Returns:
267
+ List of counterexamples found
268
+ """
269
+ if not self.is_available():
270
+ logger.warning("Alloy analyzer not available")
271
+ return []
272
+
273
+ # Write model to temp file
274
+ with tempfile.NamedTemporaryFile(
275
+ mode="w", suffix=".als", delete=False
276
+ ) as f:
277
+ f.write(model)
278
+ model_path = f.name
279
+
280
+ try:
281
+ result = subprocess.run(
282
+ ["java", "-jar", self.alloy_jar, "-c", model_path],
283
+ capture_output=True,
284
+ text=True,
285
+ timeout=self.timeout,
286
+ )
287
+
288
+ return self._parse_output(result.stdout, result.stderr)
289
+
290
+ except subprocess.TimeoutExpired:
291
+ logger.warning(f"Alloy analysis timed out after {self.timeout}s")
292
+ return [AlloyCounterexample(
293
+ assertion_name="timeout",
294
+ violated=False,
295
+ message=f"Analysis timed out after {self.timeout} seconds",
296
+ )]
297
+ except FileNotFoundError:
298
+ logger.error("Java not found - required for Alloy")
299
+ return []
300
+ finally:
301
+ Path(model_path).unlink(missing_ok=True)
302
+
303
+ def _parse_output(
304
+ self, stdout: str, stderr: str
305
+ ) -> List[AlloyCounterexample]:
306
+ """Parse Alloy analyzer output into counterexamples."""
307
+ counterexamples = []
308
+
309
+ # Parse "Assertion X may be violated" patterns
310
+ violation_pattern = r"Assertion\s+(\w+)\s+(?:may be violated|is invalid)"
311
+ for match in re.finditer(violation_pattern, stdout + stderr):
312
+ counterexamples.append(AlloyCounterexample(
313
+ assertion_name=match.group(1),
314
+ violated=True,
315
+ message="Assertion may be violated",
316
+ ))
317
+
318
+ # Parse "No counterexample found" patterns
319
+ valid_pattern = r"Assertion\s+(\w+)\s+is valid"
320
+ for match in re.finditer(valid_pattern, stdout + stderr):
321
+ counterexamples.append(AlloyCounterexample(
322
+ assertion_name=match.group(1),
323
+ violated=False,
324
+ message="No counterexample found within scope",
325
+ ))
326
+
327
+ return counterexamples
328
+
329
+ def check_assertion(
330
+ self, model: str, assertion_name: str
331
+ ) -> Tuple[bool, Optional[str]]:
332
+ """
333
+ Check a specific assertion in a model.
334
+
335
+ Args:
336
+ model: Alloy model string
337
+ assertion_name: Name of assertion to check
338
+
339
+ Returns:
340
+ Tuple of (is_valid, counterexample_message)
341
+ """
342
+ # Append check command if not present
343
+ if f"check {assertion_name}" not in model:
344
+ model = f"{model}\ncheck {assertion_name} for {self.alloy_jar or 5}"
345
+
346
+ results = self.analyze(model)
347
+
348
+ for result in results:
349
+ if result.assertion_name == assertion_name:
350
+ return (not result.violated, result.message if result.violated else None)
351
+
352
+ return (True, None) # Default: no counterexample found
@@ -0,0 +1,218 @@
1
+ """
2
+ Combined Alloy + Z3 verifier for cross-validation.
3
+
4
+ Runs both Alloy and Z3 verification and compares results
5
+ to provide higher confidence in verification outcomes.
6
+ """
7
+
8
+ from typing import List, Dict, Any, Tuple
9
+ from dataclasses import dataclass
10
+ import logging
11
+
12
+ from yuho.verify.alloy import AlloyGenerator, AlloyAnalyzer, AlloyCounterexample
13
+ from yuho.verify.z3_solver import Z3Generator, Z3Solver, Z3Diagnostic
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class CombinedVerificationResult:
20
+ """Result of combined Alloy+Z3 verification."""
21
+
22
+ alloy_available: bool
23
+ z3_available: bool
24
+
25
+ alloy_results: List[AlloyCounterexample]
26
+ z3_results: List[Z3Diagnostic]
27
+
28
+ agreement: bool # True if both agree on consistency
29
+ confidence: str # "high", "medium", "low"
30
+ message: str
31
+
32
+ def to_dict(self) -> Dict[str, Any]:
33
+ """Convert to dictionary for serialization."""
34
+ return {
35
+ "alloy_available": self.alloy_available,
36
+ "z3_available": self.z3_available,
37
+ "alloy_results": [r.__dict__ for r in self.alloy_results],
38
+ "z3_results": [r.__dict__ for r in self.z3_results],
39
+ "agreement": self.agreement,
40
+ "confidence": self.confidence,
41
+ "message": self.message,
42
+ }
43
+
44
+
45
+ class CombinedVerifier:
46
+ """
47
+ Combined verifier using both Alloy and Z3.
48
+
49
+ Provides higher confidence verification by running both
50
+ solvers and comparing their results. Disagreements are
51
+ flagged for manual review.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ alloy_jar: str | None = None,
57
+ alloy_timeout: int = 30,
58
+ z3_timeout_ms: int = 5000,
59
+ ):
60
+ """
61
+ Initialize the combined verifier.
62
+
63
+ Args:
64
+ alloy_jar: Path to Alloy JAR (None to auto-detect)
65
+ alloy_timeout: Alloy timeout in seconds
66
+ z3_timeout_ms: Z3 timeout in milliseconds
67
+ """
68
+ self.alloy_analyzer = AlloyAnalyzer(alloy_jar, alloy_timeout)
69
+ self.z3_solver = Z3Solver(z3_timeout_ms)
70
+
71
+ def verify(self, ast) -> CombinedVerificationResult:
72
+ """
73
+ Run combined verification on AST.
74
+
75
+ Args:
76
+ ast: ModuleNode from Yuho AST
77
+
78
+ Returns:
79
+ CombinedVerificationResult with comparison
80
+ """
81
+ alloy_available = self.alloy_analyzer.is_available()
82
+ z3_available = self.z3_solver.is_available()
83
+
84
+ alloy_results = []
85
+ z3_results = []
86
+
87
+ # Run Alloy verification if available
88
+ if alloy_available:
89
+ try:
90
+ alloy_gen = AlloyGenerator()
91
+ alloy_model = alloy_gen.generate(ast)
92
+ alloy_results = self.alloy_analyzer.analyze(alloy_model)
93
+ except Exception as e:
94
+ logger.error(f"Alloy verification failed: {e}")
95
+ alloy_available = False
96
+
97
+ # Run Z3 verification if available
98
+ if z3_available:
99
+ try:
100
+ is_consistent, z3_diags = self.z3_solver.check_statute_consistency(ast)
101
+ z3_results = z3_diags
102
+ except Exception as e:
103
+ logger.error(f"Z3 verification failed: {e}")
104
+ z3_available = False
105
+
106
+ # Compare results
107
+ agreement, confidence, message = self._compare_results(
108
+ alloy_available, z3_available,
109
+ alloy_results, z3_results
110
+ )
111
+
112
+ return CombinedVerificationResult(
113
+ alloy_available=alloy_available,
114
+ z3_available=z3_available,
115
+ alloy_results=alloy_results,
116
+ z3_results=z3_results,
117
+ agreement=agreement,
118
+ confidence=confidence,
119
+ message=message,
120
+ )
121
+
122
+ def _compare_results(
123
+ self,
124
+ alloy_available: bool,
125
+ z3_available: bool,
126
+ alloy_results: List[AlloyCounterexample],
127
+ z3_results: List[Z3Diagnostic],
128
+ ) -> Tuple[bool, str, str]:
129
+ """
130
+ Compare Alloy and Z3 results.
131
+
132
+ Returns:
133
+ Tuple of (agreement, confidence, message)
134
+ """
135
+ # If neither available
136
+ if not alloy_available and not z3_available:
137
+ return (False, "low", "Neither Alloy nor Z3 available")
138
+
139
+ # If only one available
140
+ if not alloy_available:
141
+ z3_passed = all(d.passed for d in z3_results)
142
+ return (
143
+ True,
144
+ "medium",
145
+ f"Z3 only: {'PASS' if z3_passed else 'FAIL'} ({len(z3_results)} checks)"
146
+ )
147
+
148
+ if not z3_available:
149
+ alloy_failed = any(r.violated for r in alloy_results)
150
+ return (
151
+ True,
152
+ "medium",
153
+ f"Alloy only: {'FAIL' if alloy_failed else 'PASS'} ({len(alloy_results)} checks)"
154
+ )
155
+
156
+ # Both available - compare
157
+ alloy_failed = any(r.violated for r in alloy_results)
158
+ z3_failed = any(not d.passed for d in z3_results)
159
+
160
+ if alloy_failed == z3_failed:
161
+ # Agreement
162
+ status = "FAIL" if alloy_failed else "PASS"
163
+ return (
164
+ True,
165
+ "high",
166
+ f"Alloy and Z3 agree: {status} (Alloy: {len(alloy_results)}, Z3: {len(z3_results)} checks)"
167
+ )
168
+ else:
169
+ # Disagreement
170
+ return (
171
+ False,
172
+ "low",
173
+ f"DISAGREEMENT: Alloy {'FAIL' if alloy_failed else 'PASS'}, "
174
+ f"Z3 {'FAIL' if z3_failed else 'PASS'} - manual review required"
175
+ )
176
+
177
+ def verify_with_details(self, ast) -> Dict[str, Any]:
178
+ """
179
+ Run verification and return detailed results.
180
+
181
+ Args:
182
+ ast: ModuleNode from Yuho AST
183
+
184
+ Returns:
185
+ Detailed results dictionary
186
+ """
187
+ result = self.verify(ast)
188
+
189
+ return {
190
+ "summary": {
191
+ "agreement": result.agreement,
192
+ "confidence": result.confidence,
193
+ "message": result.message,
194
+ },
195
+ "alloy": {
196
+ "available": result.alloy_available,
197
+ "results": [
198
+ {
199
+ "assertion": r.assertion_name,
200
+ "violated": r.violated,
201
+ "message": r.message,
202
+ }
203
+ for r in result.alloy_results
204
+ ],
205
+ },
206
+ "z3": {
207
+ "available": result.z3_available,
208
+ "results": [
209
+ {
210
+ "check": r.check_name,
211
+ "passed": r.passed,
212
+ "message": r.message,
213
+ "counterexample": r.counterexample,
214
+ }
215
+ for r in result.z3_results
216
+ ],
217
+ },
218
+ }