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,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