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/z3_solver.py
ADDED
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Z3 solver integration for Yuho constraint verification.
|
|
3
|
+
|
|
4
|
+
Provides Z3-based constraint generation and satisfiability checking
|
|
5
|
+
for pattern reachability analysis, test case generation, and
|
|
6
|
+
formal verification of statute consistency (parallel to Alloy).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
from typing import List, Dict, Any, Optional, Tuple, TYPE_CHECKING
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from yuho.ast.nodes import (
|
|
16
|
+
ModuleNode, StatuteNode, StructDefNode, ElementNode,
|
|
17
|
+
PenaltyNode, BinaryExprNode, UnaryExprNode, MatchExprNode,
|
|
18
|
+
MatchArm, ASTNode,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Try to import z3, provide stub if not available
|
|
24
|
+
try:
|
|
25
|
+
import z3
|
|
26
|
+
Z3_AVAILABLE = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
Z3_AVAILABLE = False
|
|
29
|
+
logger.debug("z3-solver not installed, Z3 features disabled")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class SatisfiabilityResult:
|
|
34
|
+
"""Result of a Z3 satisfiability check."""
|
|
35
|
+
satisfiable: bool
|
|
36
|
+
model: Optional[Dict[str, Any]] = None
|
|
37
|
+
message: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Z3Diagnostic:
|
|
42
|
+
"""A diagnostic from Z3 verification (parallel to AlloyCounterexample)."""
|
|
43
|
+
check_name: str
|
|
44
|
+
passed: bool
|
|
45
|
+
counterexample: Optional[Dict[str, Any]] = None
|
|
46
|
+
message: str = ""
|
|
47
|
+
|
|
48
|
+
def to_diagnostic(self) -> Dict[str, Any]:
|
|
49
|
+
"""Convert to LSP-compatible diagnostic."""
|
|
50
|
+
return {
|
|
51
|
+
"message": f"Z3: {self.check_name} - {self.message}",
|
|
52
|
+
"severity": "info" if self.passed else "warning",
|
|
53
|
+
"source": "z3",
|
|
54
|
+
"data": self.counterexample,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Z3CounterexampleExtractor:
|
|
59
|
+
"""
|
|
60
|
+
Extracts and converts Z3 counterexamples to human-readable diagnostics.
|
|
61
|
+
|
|
62
|
+
Uses Z3's unsat cores to identify minimal conflicting constraint sets
|
|
63
|
+
and converts satisfying models to test inputs.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self):
|
|
67
|
+
"""Initialize the extractor."""
|
|
68
|
+
if not Z3_AVAILABLE:
|
|
69
|
+
logger.warning("Z3 not available - counterexample extraction disabled")
|
|
70
|
+
|
|
71
|
+
def extract_unsat_core(
|
|
72
|
+
self, solver: Any, assertions: List[Tuple[str, Any]]
|
|
73
|
+
) -> List[str]:
|
|
74
|
+
"""
|
|
75
|
+
Extract unsat core from a solver with tracked assertions.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
solver: Z3 solver instance
|
|
79
|
+
assertions: List of (name, constraint) tuples
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
List of assertion names in the unsat core
|
|
83
|
+
"""
|
|
84
|
+
if not Z3_AVAILABLE:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
# Create solver with unsat core tracking
|
|
88
|
+
core_solver = z3.Solver()
|
|
89
|
+
core_solver.set(unsat_core=True)
|
|
90
|
+
|
|
91
|
+
# Add tracked assertions
|
|
92
|
+
tracked_assertions = {}
|
|
93
|
+
for name, constraint in assertions:
|
|
94
|
+
tracker = z3.Bool(f"track_{name}")
|
|
95
|
+
tracked_assertions[str(tracker)] = name
|
|
96
|
+
core_solver.add(z3.Implies(tracker, constraint))
|
|
97
|
+
core_solver.add(tracker) # Assume all are active initially
|
|
98
|
+
|
|
99
|
+
result = core_solver.check()
|
|
100
|
+
|
|
101
|
+
if result == z3.unsat:
|
|
102
|
+
core = core_solver.unsat_core()
|
|
103
|
+
return [tracked_assertions[str(c)] for c in core if str(c) in tracked_assertions]
|
|
104
|
+
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
def model_to_counterexample(self, model: Any) -> Dict[str, Any]:
|
|
108
|
+
"""
|
|
109
|
+
Convert Z3 model to counterexample dictionary.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
model: Z3 model from solver
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dictionary mapping variable names to values
|
|
116
|
+
"""
|
|
117
|
+
if not Z3_AVAILABLE or model is None:
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
counterexample = {}
|
|
121
|
+
|
|
122
|
+
for decl in model.decls():
|
|
123
|
+
name = decl.name()
|
|
124
|
+
value = model[decl]
|
|
125
|
+
|
|
126
|
+
# Convert Z3 values to Python primitives
|
|
127
|
+
if hasattr(value, 'as_long'):
|
|
128
|
+
counterexample[name] = value.as_long()
|
|
129
|
+
elif hasattr(value, 'as_fraction'):
|
|
130
|
+
counterexample[name] = str(value.as_fraction())
|
|
131
|
+
elif hasattr(value, 'as_string'):
|
|
132
|
+
counterexample[name] = value.as_string()
|
|
133
|
+
else:
|
|
134
|
+
counterexample[name] = str(value)
|
|
135
|
+
|
|
136
|
+
return counterexample
|
|
137
|
+
|
|
138
|
+
def generate_diagnostic_from_unsat_core(
|
|
139
|
+
self, core_names: List[str], check_name: str
|
|
140
|
+
) -> Z3Diagnostic:
|
|
141
|
+
"""
|
|
142
|
+
Generate diagnostic from unsat core.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
core_names: Names of constraints in unsat core
|
|
146
|
+
check_name: Name of the check being performed
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Z3Diagnostic with conflict information
|
|
150
|
+
"""
|
|
151
|
+
if not core_names:
|
|
152
|
+
return Z3Diagnostic(
|
|
153
|
+
check_name=check_name,
|
|
154
|
+
passed=False,
|
|
155
|
+
message="Constraints are unsatisfiable (no core extracted)"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
conflict_msg = f"Conflicting constraints: {', '.join(core_names[:5])}"
|
|
159
|
+
if len(core_names) > 5:
|
|
160
|
+
conflict_msg += f" and {len(core_names) - 5} more"
|
|
161
|
+
|
|
162
|
+
return Z3Diagnostic(
|
|
163
|
+
check_name=check_name,
|
|
164
|
+
passed=False,
|
|
165
|
+
counterexample={"unsat_core": core_names},
|
|
166
|
+
message=conflict_msg
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def generate_diagnostic_from_model(
|
|
170
|
+
self, model: Any, check_name: str, message: str
|
|
171
|
+
) -> Z3Diagnostic:
|
|
172
|
+
"""
|
|
173
|
+
Generate diagnostic from satisfying model.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
model: Z3 model
|
|
177
|
+
check_name: Name of the check
|
|
178
|
+
message: Diagnostic message
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Z3Diagnostic with model as counterexample
|
|
182
|
+
"""
|
|
183
|
+
counterexample = self.model_to_counterexample(model)
|
|
184
|
+
|
|
185
|
+
return Z3Diagnostic(
|
|
186
|
+
check_name=check_name,
|
|
187
|
+
passed=False,
|
|
188
|
+
counterexample=counterexample,
|
|
189
|
+
message=message
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ConstraintGenerator:
|
|
194
|
+
"""
|
|
195
|
+
Generates Z3 constraints from Yuho match-case conditions.
|
|
196
|
+
|
|
197
|
+
Translates Yuho expressions into Z3 formulas for
|
|
198
|
+
satisfiability checking and model extraction.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(self):
|
|
202
|
+
"""Initialize the constraint generator."""
|
|
203
|
+
if not Z3_AVAILABLE:
|
|
204
|
+
logger.warning("Z3 not available - constraint generation disabled")
|
|
205
|
+
|
|
206
|
+
self._variables: Dict[str, Any] = {}
|
|
207
|
+
|
|
208
|
+
def _get_or_create_var(self, name: str, sort: str = "int"):
|
|
209
|
+
"""Get or create a Z3 variable."""
|
|
210
|
+
if not Z3_AVAILABLE:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
if name not in self._variables:
|
|
214
|
+
if sort == "int":
|
|
215
|
+
self._variables[name] = z3.Int(name)
|
|
216
|
+
elif sort == "bool":
|
|
217
|
+
self._variables[name] = z3.Bool(name)
|
|
218
|
+
elif sort == "real":
|
|
219
|
+
self._variables[name] = z3.Real(name)
|
|
220
|
+
else:
|
|
221
|
+
self._variables[name] = z3.Int(name)
|
|
222
|
+
|
|
223
|
+
return self._variables[name]
|
|
224
|
+
|
|
225
|
+
def generate_from_condition(self, condition_str: str) -> Optional[Any]:
|
|
226
|
+
"""
|
|
227
|
+
Generate Z3 constraint from a condition string.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
condition_str: Condition expression (e.g., "x > 5 && y < 10")
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Z3 constraint or None if generation fails
|
|
234
|
+
"""
|
|
235
|
+
if not Z3_AVAILABLE:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
return self._parse_condition(condition_str)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.warning(f"Failed to generate Z3 constraint: {e}")
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
def _parse_condition(self, expr: str) -> Any:
|
|
245
|
+
"""Parse a condition expression into Z3 constraints."""
|
|
246
|
+
expr = expr.strip()
|
|
247
|
+
|
|
248
|
+
# Handle boolean operators
|
|
249
|
+
if " && " in expr:
|
|
250
|
+
parts = expr.split(" && ", 1)
|
|
251
|
+
return z3.And(
|
|
252
|
+
self._parse_condition(parts[0]),
|
|
253
|
+
self._parse_condition(parts[1])
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if " || " in expr:
|
|
257
|
+
parts = expr.split(" || ", 1)
|
|
258
|
+
return z3.Or(
|
|
259
|
+
self._parse_condition(parts[0]),
|
|
260
|
+
self._parse_condition(parts[1])
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if expr.startswith("!"):
|
|
264
|
+
return z3.Not(self._parse_condition(expr[1:].strip()))
|
|
265
|
+
|
|
266
|
+
# Handle comparisons
|
|
267
|
+
for op, z3_op in [
|
|
268
|
+
(">=", lambda a, b: a >= b),
|
|
269
|
+
("<=", lambda a, b: a <= b),
|
|
270
|
+
("!=", lambda a, b: a != b),
|
|
271
|
+
("==", lambda a, b: a == b),
|
|
272
|
+
(">", lambda a, b: a > b),
|
|
273
|
+
("<", lambda a, b: a < b),
|
|
274
|
+
]:
|
|
275
|
+
if op in expr:
|
|
276
|
+
parts = expr.split(op, 1)
|
|
277
|
+
left = self._parse_value(parts[0].strip())
|
|
278
|
+
right = self._parse_value(parts[1].strip())
|
|
279
|
+
return z3_op(left, right)
|
|
280
|
+
|
|
281
|
+
# Boolean literal or variable
|
|
282
|
+
if expr.lower() == "true":
|
|
283
|
+
return z3.BoolVal(True)
|
|
284
|
+
if expr.lower() == "false":
|
|
285
|
+
return z3.BoolVal(False)
|
|
286
|
+
|
|
287
|
+
# Variable reference
|
|
288
|
+
return self._get_or_create_var(expr, "bool")
|
|
289
|
+
|
|
290
|
+
def _parse_value(self, val: str) -> Any:
|
|
291
|
+
"""Parse a value into Z3 expression."""
|
|
292
|
+
val = val.strip()
|
|
293
|
+
|
|
294
|
+
# Try as integer
|
|
295
|
+
try:
|
|
296
|
+
return z3.IntVal(int(val))
|
|
297
|
+
except ValueError:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
# Try as float/real
|
|
301
|
+
try:
|
|
302
|
+
return z3.RealVal(float(val))
|
|
303
|
+
except ValueError:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
# Must be a variable
|
|
307
|
+
return self._get_or_create_var(val, "int")
|
|
308
|
+
|
|
309
|
+
def generate_from_match_arm(
|
|
310
|
+
self, pattern: str, guard: Optional[str] = None
|
|
311
|
+
) -> Optional[Any]:
|
|
312
|
+
"""
|
|
313
|
+
Generate constraint for a match arm.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
pattern: Match pattern (e.g., "42", "_", "x if x > 0")
|
|
317
|
+
guard: Optional guard condition
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Z3 constraint or None
|
|
321
|
+
"""
|
|
322
|
+
if not Z3_AVAILABLE:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
constraints = []
|
|
326
|
+
|
|
327
|
+
# Handle wildcard
|
|
328
|
+
if pattern.strip() == "_":
|
|
329
|
+
return z3.BoolVal(True)
|
|
330
|
+
|
|
331
|
+
# Handle literal patterns
|
|
332
|
+
try:
|
|
333
|
+
lit_val = int(pattern)
|
|
334
|
+
match_var = self._get_or_create_var("match_value")
|
|
335
|
+
constraints.append(match_var == lit_val)
|
|
336
|
+
except ValueError:
|
|
337
|
+
# Variable binding pattern
|
|
338
|
+
if pattern.isidentifier():
|
|
339
|
+
pass # No constraint, just binds
|
|
340
|
+
else:
|
|
341
|
+
# Complex pattern
|
|
342
|
+
constraints.append(self._parse_condition(pattern))
|
|
343
|
+
|
|
344
|
+
# Add guard constraint
|
|
345
|
+
if guard:
|
|
346
|
+
constraints.append(self._parse_condition(guard))
|
|
347
|
+
|
|
348
|
+
if not constraints:
|
|
349
|
+
return z3.BoolVal(True)
|
|
350
|
+
|
|
351
|
+
return z3.And(*constraints) if len(constraints) > 1 else constraints[0]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class Z3Solver:
|
|
355
|
+
"""
|
|
356
|
+
Z3-based solver for Yuho verification queries.
|
|
357
|
+
|
|
358
|
+
Supports:
|
|
359
|
+
- Pattern reachability checking
|
|
360
|
+
- Exhaustiveness verification
|
|
361
|
+
- Test case generation from satisfying models
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
def __init__(self, timeout_ms: int = 5000):
|
|
365
|
+
"""
|
|
366
|
+
Initialize the solver.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
timeout_ms: Solver timeout in milliseconds
|
|
370
|
+
"""
|
|
371
|
+
self.timeout_ms = timeout_ms
|
|
372
|
+
self.constraint_generator = ConstraintGenerator()
|
|
373
|
+
|
|
374
|
+
def is_available(self) -> bool:
|
|
375
|
+
"""Check if Z3 is available."""
|
|
376
|
+
return Z3_AVAILABLE
|
|
377
|
+
|
|
378
|
+
def check_satisfiability(
|
|
379
|
+
self, constraints: List[Any]
|
|
380
|
+
) -> SatisfiabilityResult:
|
|
381
|
+
"""
|
|
382
|
+
Check if constraints are satisfiable.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
constraints: List of Z3 constraints
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
SatisfiabilityResult with model if SAT
|
|
389
|
+
"""
|
|
390
|
+
if not Z3_AVAILABLE:
|
|
391
|
+
return SatisfiabilityResult(
|
|
392
|
+
satisfiable=False,
|
|
393
|
+
message="Z3 not available"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
solver = z3.Solver()
|
|
397
|
+
solver.set("timeout", self.timeout_ms)
|
|
398
|
+
|
|
399
|
+
for c in constraints:
|
|
400
|
+
if c is not None:
|
|
401
|
+
solver.add(c)
|
|
402
|
+
|
|
403
|
+
result = solver.check()
|
|
404
|
+
|
|
405
|
+
if result == z3.sat:
|
|
406
|
+
model = solver.model()
|
|
407
|
+
model_dict = {
|
|
408
|
+
str(d): model[d].as_long() if hasattr(model[d], 'as_long') else str(model[d])
|
|
409
|
+
for d in model.decls()
|
|
410
|
+
}
|
|
411
|
+
return SatisfiabilityResult(
|
|
412
|
+
satisfiable=True,
|
|
413
|
+
model=model_dict,
|
|
414
|
+
message="Satisfiable"
|
|
415
|
+
)
|
|
416
|
+
elif result == z3.unsat:
|
|
417
|
+
return SatisfiabilityResult(
|
|
418
|
+
satisfiable=False,
|
|
419
|
+
message="Unsatisfiable"
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
return SatisfiabilityResult(
|
|
423
|
+
satisfiable=False,
|
|
424
|
+
message="Unknown (timeout or incomplete)"
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def check_pattern_reachable(
|
|
428
|
+
self,
|
|
429
|
+
pattern: str,
|
|
430
|
+
previous_patterns: List[str],
|
|
431
|
+
) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
432
|
+
"""
|
|
433
|
+
Check if a pattern is reachable given previous patterns.
|
|
434
|
+
|
|
435
|
+
A pattern is unreachable if:
|
|
436
|
+
- It's subsumed by previous patterns (they cover all its cases)
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
pattern: The pattern to check
|
|
440
|
+
previous_patterns: Patterns that appear before it
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Tuple of (is_reachable, example_input if reachable)
|
|
444
|
+
"""
|
|
445
|
+
if not Z3_AVAILABLE:
|
|
446
|
+
return (True, None) # Assume reachable if can't verify
|
|
447
|
+
|
|
448
|
+
# Generate constraint: pattern is matched AND no previous pattern matches
|
|
449
|
+
this_constraint = self.constraint_generator.generate_from_match_arm(pattern)
|
|
450
|
+
|
|
451
|
+
prev_constraints = []
|
|
452
|
+
for prev in previous_patterns:
|
|
453
|
+
prev_c = self.constraint_generator.generate_from_match_arm(prev)
|
|
454
|
+
if prev_c is not None:
|
|
455
|
+
prev_constraints.append(z3.Not(prev_c))
|
|
456
|
+
|
|
457
|
+
# Pattern is reachable if: this_pattern AND NOT(prev1) AND NOT(prev2) ... is SAT
|
|
458
|
+
all_constraints = [this_constraint] + prev_constraints
|
|
459
|
+
|
|
460
|
+
result = self.check_satisfiability(all_constraints)
|
|
461
|
+
|
|
462
|
+
return (result.satisfiable, result.model)
|
|
463
|
+
|
|
464
|
+
def check_exhaustiveness(
|
|
465
|
+
self, patterns: List[str], type_constraints: Optional[str] = None
|
|
466
|
+
) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
467
|
+
"""
|
|
468
|
+
Check if patterns are exhaustive.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
patterns: List of patterns in the match expression
|
|
472
|
+
type_constraints: Optional type domain constraint (e.g., "x >= 0")
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Tuple of (is_exhaustive, counterexample if not exhaustive)
|
|
476
|
+
"""
|
|
477
|
+
if not Z3_AVAILABLE:
|
|
478
|
+
return (True, None) # Assume exhaustive if can't verify
|
|
479
|
+
|
|
480
|
+
# Generate constraint: none of the patterns match
|
|
481
|
+
none_match_constraints = []
|
|
482
|
+
for pattern in patterns:
|
|
483
|
+
pattern_c = self.constraint_generator.generate_from_match_arm(pattern)
|
|
484
|
+
if pattern_c is not None:
|
|
485
|
+
none_match_constraints.append(z3.Not(pattern_c))
|
|
486
|
+
|
|
487
|
+
# Add type constraints if provided
|
|
488
|
+
all_constraints = none_match_constraints
|
|
489
|
+
if type_constraints:
|
|
490
|
+
type_c = self.constraint_generator.generate_from_condition(type_constraints)
|
|
491
|
+
if type_c is not None:
|
|
492
|
+
all_constraints.append(type_c)
|
|
493
|
+
|
|
494
|
+
# If SAT, patterns are not exhaustive (found an input that matches nothing)
|
|
495
|
+
result = self.check_satisfiability(all_constraints)
|
|
496
|
+
|
|
497
|
+
if result.satisfiable:
|
|
498
|
+
return (False, result.model) # Found counterexample
|
|
499
|
+
else:
|
|
500
|
+
return (True, None) # Exhaustive
|
|
501
|
+
|
|
502
|
+
def generate_test_case(
|
|
503
|
+
self, constraints: List[str]
|
|
504
|
+
) -> Optional[Dict[str, Any]]:
|
|
505
|
+
"""
|
|
506
|
+
Generate a concrete test case satisfying constraints.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
constraints: List of constraint expressions
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
Dictionary of variable assignments or None if UNSAT
|
|
513
|
+
"""
|
|
514
|
+
if not Z3_AVAILABLE:
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
z3_constraints = []
|
|
518
|
+
for c in constraints:
|
|
519
|
+
z3_c = self.constraint_generator.generate_from_condition(c)
|
|
520
|
+
if z3_c is not None:
|
|
521
|
+
z3_constraints.append(z3_c)
|
|
522
|
+
|
|
523
|
+
result = self.check_satisfiability(z3_constraints)
|
|
524
|
+
return result.model if result.satisfiable else None
|
|
525
|
+
|
|
526
|
+
def enumerate_models(
|
|
527
|
+
self, constraints: List[Any], max_models: int = 10
|
|
528
|
+
) -> List[Dict[str, Any]]:
|
|
529
|
+
"""
|
|
530
|
+
Enumerate multiple satisfying models for given constraints.
|
|
531
|
+
|
|
532
|
+
Finds up to max_models different satisfying assignments that
|
|
533
|
+
fulfill the constraints. Useful for test case generation and
|
|
534
|
+
exploring solution spaces.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
constraints: List of Z3 constraints
|
|
538
|
+
max_models: Maximum number of models to enumerate
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
List of model dictionaries (empty if UNSAT)
|
|
542
|
+
"""
|
|
543
|
+
if not Z3_AVAILABLE:
|
|
544
|
+
return []
|
|
545
|
+
|
|
546
|
+
solver = z3.Solver()
|
|
547
|
+
solver.set("timeout", self.timeout_ms)
|
|
548
|
+
|
|
549
|
+
# Add all constraints
|
|
550
|
+
for c in constraints:
|
|
551
|
+
if c is not None:
|
|
552
|
+
solver.add(c)
|
|
553
|
+
|
|
554
|
+
models = []
|
|
555
|
+
extractor = Z3CounterexampleExtractor()
|
|
556
|
+
|
|
557
|
+
for _ in range(max_models):
|
|
558
|
+
result = solver.check()
|
|
559
|
+
|
|
560
|
+
if result != z3.sat:
|
|
561
|
+
break
|
|
562
|
+
|
|
563
|
+
# Extract model
|
|
564
|
+
model = solver.model()
|
|
565
|
+
model_dict = extractor.model_to_counterexample(model)
|
|
566
|
+
models.append(model_dict)
|
|
567
|
+
|
|
568
|
+
# Block this solution to find a different one
|
|
569
|
+
block_constraint = []
|
|
570
|
+
for decl in model.decls():
|
|
571
|
+
const = decl()
|
|
572
|
+
value = model[decl]
|
|
573
|
+
# Add constraint that at least one variable must differ
|
|
574
|
+
block_constraint.append(const != value)
|
|
575
|
+
|
|
576
|
+
if block_constraint:
|
|
577
|
+
solver.add(z3.Or(block_constraint))
|
|
578
|
+
else:
|
|
579
|
+
# No variables to block, can't enumerate more
|
|
580
|
+
break
|
|
581
|
+
|
|
582
|
+
return models
|
|
583
|
+
|
|
584
|
+
def enumerate_statute_interpretations(
|
|
585
|
+
self, ast: "ModuleNode", max_interpretations: int = 5
|
|
586
|
+
) -> List[Dict[str, Any]]:
|
|
587
|
+
"""
|
|
588
|
+
Enumerate valid interpretations of statute constraints.
|
|
589
|
+
|
|
590
|
+
Finds multiple satisfying models that represent different
|
|
591
|
+
valid interpretations of the statutes.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
ast: ModuleNode from Yuho AST
|
|
595
|
+
max_interpretations: Maximum interpretations to find
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
List of interpretation dictionaries
|
|
599
|
+
"""
|
|
600
|
+
generator = Z3Generator()
|
|
601
|
+
solver, assertions = generator.generate(ast)
|
|
602
|
+
|
|
603
|
+
if not Z3_AVAILABLE or solver is None:
|
|
604
|
+
return []
|
|
605
|
+
|
|
606
|
+
interpretations = []
|
|
607
|
+
extractor = Z3CounterexampleExtractor()
|
|
608
|
+
|
|
609
|
+
for i in range(max_interpretations):
|
|
610
|
+
result = solver.check()
|
|
611
|
+
|
|
612
|
+
if result != z3.sat:
|
|
613
|
+
break
|
|
614
|
+
|
|
615
|
+
model = solver.model()
|
|
616
|
+
interpretation = extractor.model_to_counterexample(model)
|
|
617
|
+
interpretation['interpretation_id'] = i + 1
|
|
618
|
+
interpretations.append(interpretation)
|
|
619
|
+
|
|
620
|
+
# Block this interpretation
|
|
621
|
+
block_constraints = []
|
|
622
|
+
for decl in model.decls():
|
|
623
|
+
const = decl()
|
|
624
|
+
value = model[decl]
|
|
625
|
+
block_constraints.append(const != value)
|
|
626
|
+
|
|
627
|
+
if block_constraints:
|
|
628
|
+
solver.add(z3.Or(block_constraints))
|
|
629
|
+
else:
|
|
630
|
+
break
|
|
631
|
+
|
|
632
|
+
return interpretations
|
|
633
|
+
|
|
634
|
+
def check_statute_consistency(
|
|
635
|
+
self, ast: "ModuleNode"
|
|
636
|
+
) -> Tuple[bool, List[Z3Diagnostic]]:
|
|
637
|
+
"""
|
|
638
|
+
Check statute consistency using Z3 satisfiability.
|
|
639
|
+
|
|
640
|
+
Generates constraints from the AST and verifies they are consistent
|
|
641
|
+
(satisfiable). Returns diagnostics for any issues found.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
ast: ModuleNode from Yuho AST
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
Tuple of (is_consistent, list of diagnostics)
|
|
648
|
+
"""
|
|
649
|
+
generator = Z3Generator()
|
|
650
|
+
diagnostics = generator.generate_consistency_check(ast)
|
|
651
|
+
|
|
652
|
+
is_consistent = all(d.passed for d in diagnostics)
|
|
653
|
+
return (is_consistent, diagnostics)
|
|
654
|
+
|
|
655
|
+
def verify_statute_elements(
|
|
656
|
+
self, ast: "ModuleNode"
|
|
657
|
+
) -> List[Z3Diagnostic]:
|
|
658
|
+
"""
|
|
659
|
+
Verify that statute elements are well-formed and consistent.
|
|
660
|
+
|
|
661
|
+
Checks:
|
|
662
|
+
- Element names are unique within each statute
|
|
663
|
+
- Element types are valid (actus_reus, mens_rea, circumstance)
|
|
664
|
+
- Penalties have valid ranges (min <= max)
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
ast: ModuleNode from Yuho AST
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
List of Z3Diagnostic results
|
|
671
|
+
"""
|
|
672
|
+
diagnostics = []
|
|
673
|
+
|
|
674
|
+
for statute in ast.statutes:
|
|
675
|
+
statute_id = statute.section_number
|
|
676
|
+
|
|
677
|
+
# Check element uniqueness
|
|
678
|
+
element_names = [e.name for e in statute.elements]
|
|
679
|
+
if len(element_names) != len(set(element_names)):
|
|
680
|
+
duplicates = [n for n in element_names if element_names.count(n) > 1]
|
|
681
|
+
diagnostics.append(Z3Diagnostic(
|
|
682
|
+
check_name=f"{statute_id}_element_uniqueness",
|
|
683
|
+
passed=False,
|
|
684
|
+
message=f"Duplicate element names: {set(duplicates)}"
|
|
685
|
+
))
|
|
686
|
+
else:
|
|
687
|
+
diagnostics.append(Z3Diagnostic(
|
|
688
|
+
check_name=f"{statute_id}_element_uniqueness",
|
|
689
|
+
passed=True,
|
|
690
|
+
message="All element names are unique"
|
|
691
|
+
))
|
|
692
|
+
|
|
693
|
+
# Check element types
|
|
694
|
+
valid_types = {"actus_reus", "mens_rea", "circumstance"}
|
|
695
|
+
for element in statute.elements:
|
|
696
|
+
if element.element_type not in valid_types:
|
|
697
|
+
diagnostics.append(Z3Diagnostic(
|
|
698
|
+
check_name=f"{statute_id}_{element.name}_type",
|
|
699
|
+
passed=False,
|
|
700
|
+
message=f"Invalid element type: {element.element_type}"
|
|
701
|
+
))
|
|
702
|
+
|
|
703
|
+
# Check penalty ranges
|
|
704
|
+
if statute.penalty:
|
|
705
|
+
penalty = statute.penalty
|
|
706
|
+
|
|
707
|
+
# Check imprisonment range
|
|
708
|
+
if penalty.imprisonment_min and penalty.imprisonment_max:
|
|
709
|
+
min_days = penalty.imprisonment_min.total_days()
|
|
710
|
+
max_days = penalty.imprisonment_max.total_days()
|
|
711
|
+
if min_days > max_days:
|
|
712
|
+
diagnostics.append(Z3Diagnostic(
|
|
713
|
+
check_name=f"{statute_id}_imprisonment_range",
|
|
714
|
+
passed=False,
|
|
715
|
+
message=f"Imprisonment min ({min_days} days) > max ({max_days} days)"
|
|
716
|
+
))
|
|
717
|
+
else:
|
|
718
|
+
diagnostics.append(Z3Diagnostic(
|
|
719
|
+
check_name=f"{statute_id}_imprisonment_range",
|
|
720
|
+
passed=True,
|
|
721
|
+
message="Imprisonment range is valid"
|
|
722
|
+
))
|
|
723
|
+
|
|
724
|
+
# Check fine range
|
|
725
|
+
if penalty.fine_min and penalty.fine_max:
|
|
726
|
+
if penalty.fine_min.amount > penalty.fine_max.amount:
|
|
727
|
+
diagnostics.append(Z3Diagnostic(
|
|
728
|
+
check_name=f"{statute_id}_fine_range",
|
|
729
|
+
passed=False,
|
|
730
|
+
message=f"Fine min ({penalty.fine_min.amount}) > max ({penalty.fine_max.amount})"
|
|
731
|
+
))
|
|
732
|
+
else:
|
|
733
|
+
diagnostics.append(Z3Diagnostic(
|
|
734
|
+
check_name=f"{statute_id}_fine_range",
|
|
735
|
+
passed=True,
|
|
736
|
+
message="Fine range is valid"
|
|
737
|
+
))
|
|
738
|
+
|
|
739
|
+
return diagnostics
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def check_match_exhaustiveness(
|
|
743
|
+
self, match_expr: "MatchExprNode"
|
|
744
|
+
) -> Tuple[bool, List[Z3Diagnostic]]:
|
|
745
|
+
"""
|
|
746
|
+
Check if a match expression is exhaustive using Z3.
|
|
747
|
+
|
|
748
|
+
Analyzes match arms to determine if all possible values of the
|
|
749
|
+
scrutinee are covered. Returns diagnostics with counterexamples
|
|
750
|
+
if match is non-exhaustive.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
match_expr: MatchExprNode from Yuho AST
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
Tuple of (is_exhaustive, list of diagnostics)
|
|
757
|
+
"""
|
|
758
|
+
from yuho.ast.nodes import (
|
|
759
|
+
WildcardPattern, LiteralPattern, BindingPattern,
|
|
760
|
+
StructPattern, IntLit, StringLit, BoolLit
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
diagnostics = []
|
|
764
|
+
|
|
765
|
+
# If no scrutinee (bare match block), can't check exhaustiveness
|
|
766
|
+
if match_expr.scrutinee is None:
|
|
767
|
+
return (True, diagnostics)
|
|
768
|
+
|
|
769
|
+
# Check if there's a wildcard pattern (always exhaustive)
|
|
770
|
+
has_wildcard = any(
|
|
771
|
+
isinstance(arm.pattern, WildcardPattern) or
|
|
772
|
+
isinstance(arm.pattern, BindingPattern)
|
|
773
|
+
for arm in match_expr.arms
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
if has_wildcard:
|
|
777
|
+
diagnostics.append(Z3Diagnostic(
|
|
778
|
+
check_name="match_exhaustiveness",
|
|
779
|
+
passed=True,
|
|
780
|
+
message="Match has wildcard pattern (exhaustive)"
|
|
781
|
+
))
|
|
782
|
+
return (True, diagnostics)
|
|
783
|
+
|
|
784
|
+
# Extract pattern strings for Z3 checking
|
|
785
|
+
patterns = []
|
|
786
|
+
for arm in match_expr.arms:
|
|
787
|
+
pattern_str = self._pattern_to_constraint_string(arm.pattern)
|
|
788
|
+
if pattern_str:
|
|
789
|
+
patterns.append(pattern_str)
|
|
790
|
+
|
|
791
|
+
# Check exhaustiveness with Z3
|
|
792
|
+
is_exhaustive, counterexample = self.check_exhaustiveness(patterns)
|
|
793
|
+
|
|
794
|
+
if is_exhaustive:
|
|
795
|
+
diagnostics.append(Z3Diagnostic(
|
|
796
|
+
check_name="match_exhaustiveness",
|
|
797
|
+
passed=True,
|
|
798
|
+
message=f"Match expression is exhaustive ({len(patterns)} arms)"
|
|
799
|
+
))
|
|
800
|
+
else:
|
|
801
|
+
extractor = Z3CounterexampleExtractor()
|
|
802
|
+
if counterexample:
|
|
803
|
+
diagnostic = extractor.generate_diagnostic_from_model(
|
|
804
|
+
None, # Would need Z3 model object here
|
|
805
|
+
"match_exhaustiveness",
|
|
806
|
+
f"Match is non-exhaustive, missing case for: {counterexample}"
|
|
807
|
+
)
|
|
808
|
+
diagnostic = Z3Diagnostic(
|
|
809
|
+
check_name="match_exhaustiveness",
|
|
810
|
+
passed=False,
|
|
811
|
+
counterexample=counterexample,
|
|
812
|
+
message=f"Match is non-exhaustive, example missing value: {counterexample}"
|
|
813
|
+
)
|
|
814
|
+
else:
|
|
815
|
+
diagnostic = Z3Diagnostic(
|
|
816
|
+
check_name="match_exhaustiveness",
|
|
817
|
+
passed=False,
|
|
818
|
+
message="Match is non-exhaustive (no counterexample generated)"
|
|
819
|
+
)
|
|
820
|
+
diagnostics.append(diagnostic)
|
|
821
|
+
|
|
822
|
+
return (is_exhaustive, diagnostics)
|
|
823
|
+
|
|
824
|
+
def _pattern_to_constraint_string(self, pattern: "ASTNode") -> Optional[str]:
|
|
825
|
+
"""
|
|
826
|
+
Convert a pattern AST node to a constraint string for Z3.
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
pattern: Pattern node from AST
|
|
830
|
+
|
|
831
|
+
Returns:
|
|
832
|
+
Constraint string or None if pattern can't be converted
|
|
833
|
+
"""
|
|
834
|
+
from yuho.ast.nodes import (
|
|
835
|
+
WildcardPattern, LiteralPattern, BindingPattern,
|
|
836
|
+
IntLit, StringLit, BoolLit
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
if isinstance(pattern, WildcardPattern):
|
|
840
|
+
return "_"
|
|
841
|
+
|
|
842
|
+
if isinstance(pattern, BindingPattern):
|
|
843
|
+
return pattern.name
|
|
844
|
+
|
|
845
|
+
if isinstance(pattern, LiteralPattern):
|
|
846
|
+
lit = pattern.literal
|
|
847
|
+
if isinstance(lit, IntLit):
|
|
848
|
+
return str(lit.value)
|
|
849
|
+
elif isinstance(lit, BoolLit):
|
|
850
|
+
return "true" if lit.value else "false"
|
|
851
|
+
elif isinstance(lit, StringLit):
|
|
852
|
+
return f'"{lit.value}"'
|
|
853
|
+
|
|
854
|
+
# For other patterns, return None (can't easily convert)
|
|
855
|
+
return None
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
class Z3Generator:
|
|
859
|
+
"""
|
|
860
|
+
Generates Z3 constraints from Yuho statute ASTs.
|
|
861
|
+
|
|
862
|
+
This is the Z3 parallel to AlloyGenerator. It translates statute
|
|
863
|
+
elements, penalties, and constraints into Z3 assertions for
|
|
864
|
+
satisfiability checking and verification.
|
|
865
|
+
"""
|
|
866
|
+
|
|
867
|
+
def __init__(self):
|
|
868
|
+
"""Initialize the generator."""
|
|
869
|
+
if not Z3_AVAILABLE:
|
|
870
|
+
logger.warning("Z3 not available - constraint generation disabled")
|
|
871
|
+
|
|
872
|
+
self._sorts: Dict[str, Any] = {} # Custom Z3 sorts
|
|
873
|
+
self._consts: Dict[str, Any] = {} # Declared constants
|
|
874
|
+
self._assertions: List[Any] = [] # Collected assertions
|
|
875
|
+
|
|
876
|
+
def generate(self, ast: "ModuleNode") -> Tuple[Any, List[Any]]:
|
|
877
|
+
"""
|
|
878
|
+
Generate Z3 solver and constraints from a module AST.
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
ast: ModuleNode from Yuho AST
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Tuple of (Z3 Solver with constraints, list of named assertions)
|
|
885
|
+
"""
|
|
886
|
+
if not Z3_AVAILABLE:
|
|
887
|
+
return None, []
|
|
888
|
+
|
|
889
|
+
self._sorts = {}
|
|
890
|
+
self._consts = {}
|
|
891
|
+
self._assertions = []
|
|
892
|
+
|
|
893
|
+
solver = z3.Solver()
|
|
894
|
+
|
|
895
|
+
# Generate sorts (types) from struct definitions
|
|
896
|
+
self._generate_sorts(ast)
|
|
897
|
+
|
|
898
|
+
# Generate constraints from statutes
|
|
899
|
+
for statute in ast.statutes:
|
|
900
|
+
self._generate_statute_constraints(statute)
|
|
901
|
+
|
|
902
|
+
# Add all collected assertions
|
|
903
|
+
for assertion in self._assertions:
|
|
904
|
+
solver.add(assertion)
|
|
905
|
+
|
|
906
|
+
return solver, self._assertions
|
|
907
|
+
|
|
908
|
+
def _generate_sorts(self, ast: "ModuleNode") -> None:
|
|
909
|
+
"""Generate Z3 sorts from type definitions."""
|
|
910
|
+
if not Z3_AVAILABLE:
|
|
911
|
+
return
|
|
912
|
+
|
|
913
|
+
# Base sorts
|
|
914
|
+
self._sorts["Person"] = z3.DeclareSort("Person")
|
|
915
|
+
self._sorts["Intent"] = z3.DeclareSort("Intent")
|
|
916
|
+
self._sorts["Element"] = z3.DeclareSort("Element")
|
|
917
|
+
|
|
918
|
+
# Intent enum values as constants
|
|
919
|
+
self._consts["Intentional"] = z3.Const("Intentional", self._sorts["Intent"])
|
|
920
|
+
self._consts["Reckless"] = z3.Const("Reckless", self._sorts["Intent"])
|
|
921
|
+
self._consts["Negligent"] = z3.Const("Negligent", self._sorts["Intent"])
|
|
922
|
+
|
|
923
|
+
# Distinct intent values
|
|
924
|
+
self._assertions.append(
|
|
925
|
+
z3.Distinct(
|
|
926
|
+
self._consts["Intentional"],
|
|
927
|
+
self._consts["Reckless"],
|
|
928
|
+
self._consts["Negligent"]
|
|
929
|
+
)
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
# Generate from struct definitions
|
|
933
|
+
for struct_def in ast.type_defs:
|
|
934
|
+
sort = z3.DeclareSort(struct_def.name)
|
|
935
|
+
self._sorts[struct_def.name] = sort
|
|
936
|
+
|
|
937
|
+
# Create symbolic field accessors
|
|
938
|
+
for field_def in struct_def.fields:
|
|
939
|
+
field_sort = self._type_to_sort(field_def.type_annotation)
|
|
940
|
+
func_name = f"{struct_def.name}_{field_def.name}"
|
|
941
|
+
self._consts[func_name] = z3.Function(
|
|
942
|
+
func_name, sort, field_sort
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
def _generate_statute_constraints(self, statute: "StatuteNode") -> None:
|
|
946
|
+
"""Generate Z3 constraints from a statute."""
|
|
947
|
+
if not Z3_AVAILABLE:
|
|
948
|
+
return
|
|
949
|
+
|
|
950
|
+
statute_id = statute.section_number.replace(".", "_")
|
|
951
|
+
|
|
952
|
+
# Create a symbolic constant for this statute instance
|
|
953
|
+
if "Statute" not in self._sorts:
|
|
954
|
+
self._sorts["Statute"] = z3.DeclareSort("Statute")
|
|
955
|
+
|
|
956
|
+
statute_const = z3.Const(f"statute_{statute_id}", self._sorts["Statute"])
|
|
957
|
+
self._consts[f"statute_{statute_id}"] = statute_const
|
|
958
|
+
|
|
959
|
+
# Generate element constraints
|
|
960
|
+
element_satisfied = []
|
|
961
|
+
for i, element in enumerate(statute.elements):
|
|
962
|
+
elem_name = element.name.replace(" ", "_").replace("-", "_")
|
|
963
|
+
elem_var = z3.Bool(f"{statute_id}_{elem_name}_satisfied")
|
|
964
|
+
self._consts[f"{statute_id}_{elem_name}"] = elem_var
|
|
965
|
+
element_satisfied.append(elem_var)
|
|
966
|
+
|
|
967
|
+
# Element-specific constraints based on type
|
|
968
|
+
if element.element_type == "mens_rea":
|
|
969
|
+
# Mens rea elements require intent
|
|
970
|
+
intent_var = z3.Const(
|
|
971
|
+
f"{statute_id}_{elem_name}_intent",
|
|
972
|
+
self._sorts["Intent"]
|
|
973
|
+
)
|
|
974
|
+
self._consts[f"{statute_id}_{elem_name}_intent"] = intent_var
|
|
975
|
+
|
|
976
|
+
# All elements must be satisfied for conviction
|
|
977
|
+
if element_satisfied:
|
|
978
|
+
all_elements = z3.And(*element_satisfied)
|
|
979
|
+
conviction_var = z3.Bool(f"{statute_id}_conviction")
|
|
980
|
+
self._consts[f"{statute_id}_conviction"] = conviction_var
|
|
981
|
+
|
|
982
|
+
# conviction <=> all elements satisfied
|
|
983
|
+
self._assertions.append(conviction_var == all_elements)
|
|
984
|
+
|
|
985
|
+
# Generate penalty constraints
|
|
986
|
+
if statute.penalty:
|
|
987
|
+
self._generate_penalty_constraints(statute_id, statute.penalty)
|
|
988
|
+
|
|
989
|
+
def _generate_penalty_constraints(
|
|
990
|
+
self, statute_id: str, penalty: "PenaltyNode"
|
|
991
|
+
) -> None:
|
|
992
|
+
"""Generate Z3 constraints for penalty specification."""
|
|
993
|
+
if not Z3_AVAILABLE:
|
|
994
|
+
return
|
|
995
|
+
|
|
996
|
+
# Imprisonment duration constraints (in days)
|
|
997
|
+
if penalty.imprisonment_min or penalty.imprisonment_max:
|
|
998
|
+
imprisonment = z3.Int(f"{statute_id}_imprisonment_days")
|
|
999
|
+
self._consts[f"{statute_id}_imprisonment"] = imprisonment
|
|
1000
|
+
|
|
1001
|
+
if penalty.imprisonment_min:
|
|
1002
|
+
min_days = penalty.imprisonment_min.total_days()
|
|
1003
|
+
self._assertions.append(imprisonment >= min_days)
|
|
1004
|
+
|
|
1005
|
+
if penalty.imprisonment_max:
|
|
1006
|
+
max_days = penalty.imprisonment_max.total_days()
|
|
1007
|
+
self._assertions.append(imprisonment <= max_days)
|
|
1008
|
+
|
|
1009
|
+
# Imprisonment must be non-negative
|
|
1010
|
+
self._assertions.append(imprisonment >= 0)
|
|
1011
|
+
|
|
1012
|
+
# Fine constraints (in cents for precision)
|
|
1013
|
+
if penalty.fine_min or penalty.fine_max:
|
|
1014
|
+
fine = z3.Int(f"{statute_id}_fine_cents")
|
|
1015
|
+
self._consts[f"{statute_id}_fine"] = fine
|
|
1016
|
+
|
|
1017
|
+
if penalty.fine_min:
|
|
1018
|
+
min_cents = int(penalty.fine_min.amount * 100)
|
|
1019
|
+
self._assertions.append(fine >= min_cents)
|
|
1020
|
+
|
|
1021
|
+
if penalty.fine_max:
|
|
1022
|
+
max_cents = int(penalty.fine_max.amount * 100)
|
|
1023
|
+
self._assertions.append(fine <= max_cents)
|
|
1024
|
+
|
|
1025
|
+
# Fine must be non-negative
|
|
1026
|
+
self._assertions.append(fine >= 0)
|
|
1027
|
+
|
|
1028
|
+
def _type_to_sort(self, type_node: "ASTNode") -> Any:
|
|
1029
|
+
"""Convert Yuho type to Z3 sort."""
|
|
1030
|
+
if not Z3_AVAILABLE:
|
|
1031
|
+
return None
|
|
1032
|
+
|
|
1033
|
+
# Import here to avoid circular imports
|
|
1034
|
+
from yuho.ast.nodes import BuiltinType, NamedType, ArrayType, OptionalType
|
|
1035
|
+
|
|
1036
|
+
if isinstance(type_node, BuiltinType):
|
|
1037
|
+
type_map = {
|
|
1038
|
+
"int": z3.IntSort(),
|
|
1039
|
+
"float": z3.RealSort(),
|
|
1040
|
+
"bool": z3.BoolSort(),
|
|
1041
|
+
"string": z3.StringSort(),
|
|
1042
|
+
"money": z3.IntSort(), # Cents
|
|
1043
|
+
"percent": z3.IntSort(), # Basis points
|
|
1044
|
+
"date": z3.IntSort(), # Unix timestamp
|
|
1045
|
+
"duration": z3.IntSort(), # Days
|
|
1046
|
+
"void": z3.BoolSort(), # Placeholder
|
|
1047
|
+
}
|
|
1048
|
+
return type_map.get(type_node.name, z3.IntSort())
|
|
1049
|
+
|
|
1050
|
+
elif isinstance(type_node, NamedType):
|
|
1051
|
+
if type_node.name in self._sorts:
|
|
1052
|
+
return self._sorts[type_node.name]
|
|
1053
|
+
# Create a new sort for unknown named types
|
|
1054
|
+
sort = z3.DeclareSort(type_node.name)
|
|
1055
|
+
self._sorts[type_node.name] = sort
|
|
1056
|
+
return sort
|
|
1057
|
+
|
|
1058
|
+
elif isinstance(type_node, ArrayType):
|
|
1059
|
+
elem_sort = self._type_to_sort(type_node.element_type)
|
|
1060
|
+
return z3.ArraySort(z3.IntSort(), elem_sort)
|
|
1061
|
+
|
|
1062
|
+
elif isinstance(type_node, OptionalType):
|
|
1063
|
+
# Model optional as the inner type (None handled separately)
|
|
1064
|
+
return self._type_to_sort(type_node.inner)
|
|
1065
|
+
|
|
1066
|
+
# Default to Int
|
|
1067
|
+
return z3.IntSort()
|
|
1068
|
+
|
|
1069
|
+
def generate_consistency_check(self, ast: "ModuleNode") -> List[Z3Diagnostic]:
|
|
1070
|
+
"""
|
|
1071
|
+
Generate and check consistency constraints for all statutes.
|
|
1072
|
+
|
|
1073
|
+
Args:
|
|
1074
|
+
ast: ModuleNode from Yuho AST
|
|
1075
|
+
|
|
1076
|
+
Returns:
|
|
1077
|
+
List of Z3Diagnostic results
|
|
1078
|
+
"""
|
|
1079
|
+
if not Z3_AVAILABLE:
|
|
1080
|
+
return [Z3Diagnostic(
|
|
1081
|
+
check_name="z3_availability",
|
|
1082
|
+
passed=False,
|
|
1083
|
+
message="Z3 solver not available"
|
|
1084
|
+
)]
|
|
1085
|
+
|
|
1086
|
+
diagnostics = []
|
|
1087
|
+
solver, assertions = self.generate(ast)
|
|
1088
|
+
|
|
1089
|
+
if solver is None:
|
|
1090
|
+
return diagnostics
|
|
1091
|
+
|
|
1092
|
+
extractor = Z3CounterexampleExtractor()
|
|
1093
|
+
|
|
1094
|
+
# Check overall satisfiability
|
|
1095
|
+
result = solver.check()
|
|
1096
|
+
if result == z3.sat:
|
|
1097
|
+
diagnostics.append(Z3Diagnostic(
|
|
1098
|
+
check_name="statute_consistency",
|
|
1099
|
+
passed=True,
|
|
1100
|
+
message="All statute constraints are satisfiable"
|
|
1101
|
+
))
|
|
1102
|
+
elif result == z3.unsat:
|
|
1103
|
+
# Extract unsat core for better diagnostics
|
|
1104
|
+
# Need to regenerate with tracked assertions
|
|
1105
|
+
tracked_assertions = []
|
|
1106
|
+
for i, assertion in enumerate(assertions):
|
|
1107
|
+
tracked_assertions.append((f"assertion_{i}", assertion))
|
|
1108
|
+
|
|
1109
|
+
core_names = extractor.extract_unsat_core(solver, tracked_assertions)
|
|
1110
|
+
|
|
1111
|
+
if core_names:
|
|
1112
|
+
diagnostic = extractor.generate_diagnostic_from_unsat_core(
|
|
1113
|
+
core_names, "statute_consistency"
|
|
1114
|
+
)
|
|
1115
|
+
diagnostics.append(diagnostic)
|
|
1116
|
+
else:
|
|
1117
|
+
diagnostics.append(Z3Diagnostic(
|
|
1118
|
+
check_name="statute_consistency",
|
|
1119
|
+
passed=False,
|
|
1120
|
+
message="Statute constraints are contradictory"
|
|
1121
|
+
))
|
|
1122
|
+
else:
|
|
1123
|
+
diagnostics.append(Z3Diagnostic(
|
|
1124
|
+
check_name="statute_consistency",
|
|
1125
|
+
passed=False,
|
|
1126
|
+
message="Could not determine consistency (timeout or unknown)"
|
|
1127
|
+
))
|
|
1128
|
+
|
|
1129
|
+
# Check penalty ordering (if multiple statutes)
|
|
1130
|
+
if len(ast.statutes) > 1:
|
|
1131
|
+
penalty_check = self._check_penalty_ordering(ast)
|
|
1132
|
+
diagnostics.extend(penalty_check)
|
|
1133
|
+
|
|
1134
|
+
return diagnostics
|
|
1135
|
+
|
|
1136
|
+
def _check_penalty_ordering(self, ast: "ModuleNode") -> List[Z3Diagnostic]:
|
|
1137
|
+
"""Check that penalty ranges don't have unexpected overlaps."""
|
|
1138
|
+
diagnostics = []
|
|
1139
|
+
|
|
1140
|
+
# Collect statutes with penalties
|
|
1141
|
+
statutes_with_penalties = [
|
|
1142
|
+
s for s in ast.statutes if s.penalty is not None
|
|
1143
|
+
]
|
|
1144
|
+
|
|
1145
|
+
if len(statutes_with_penalties) < 2:
|
|
1146
|
+
return diagnostics
|
|
1147
|
+
|
|
1148
|
+
# This is a placeholder for more sophisticated penalty analysis
|
|
1149
|
+
diagnostics.append(Z3Diagnostic(
|
|
1150
|
+
check_name="penalty_ordering",
|
|
1151
|
+
passed=True,
|
|
1152
|
+
message=f"Analyzed {len(statutes_with_penalties)} statutes with penalties"
|
|
1153
|
+
))
|
|
1154
|
+
|
|
1155
|
+
return diagnostics
|