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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- 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
|
yuho/verify/combined.py
ADDED
|
@@ -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
|
+
}
|