mirror of
https://github.com/Z3Prover/z3
synced 2025-09-11 20:21:25 +00:00
Add comprehensive documentation and examples for UserPropagator quantifier instantiation callbacks
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
This commit is contained in:
parent
f3f0171f35
commit
2e031bc7fc
6 changed files with 1809 additions and 0 deletions
293
doc/quantifier_instantiation_callback.md
Normal file
293
doc/quantifier_instantiation_callback.md
Normal file
|
@ -0,0 +1,293 @@
|
|||
# UserPropagator Quantifier Instantiation Callback in Z3
|
||||
|
||||
This document describes the UserPropagator callback for quantifier instantiations feature added in Z3 version 4.15.3.
|
||||
|
||||
## Overview
|
||||
|
||||
The quantifier instantiation callback allows user propagators to intercept and control quantifier instantiations. When Z3 attempts to instantiate a quantifier, the callback is invoked with the quantifier and its proposed instantiation. The callback can return `false` to discard the instantiation, providing fine-grained control over the quantifier instantiation process.
|
||||
|
||||
This feature enables:
|
||||
1. **Inspection and logging** of instantiation patterns
|
||||
2. **Filtering** of undesired instantiations
|
||||
3. **Custom instantiation strategies**
|
||||
4. **Performance optimization** by delaying certain instantiations
|
||||
|
||||
## API Reference
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
from z3 import *
|
||||
|
||||
class MyUserPropagator(UserPropagateBase):
|
||||
def __init__(self, s=None, ctx=None):
|
||||
UserPropagateBase.__init__(self, s, ctx)
|
||||
# Register the quantifier instantiation callback
|
||||
self.add_on_binding(self.my_callback)
|
||||
|
||||
def my_callback(self, quantifier, instantiation):
|
||||
"""
|
||||
Callback for quantifier instantiation control.
|
||||
|
||||
Args:
|
||||
quantifier: The quantifier being instantiated (Z3 AST)
|
||||
instantiation: The proposed instantiation (Z3 AST)
|
||||
|
||||
Returns:
|
||||
bool: True to allow the instantiation, False to discard it
|
||||
"""
|
||||
# Your logic here
|
||||
return True # or False to block
|
||||
|
||||
# Required methods
|
||||
def push(self): pass
|
||||
def pop(self, num_scopes): pass
|
||||
def fresh(self, new_ctx): return MyUserPropagator(ctx=new_ctx)
|
||||
```
|
||||
|
||||
### C API
|
||||
|
||||
```c
|
||||
#include <z3.h>
|
||||
|
||||
// Callback function signature
|
||||
Z3_bool my_callback(void* ctx, Z3_solver_callback cb, Z3_ast q, Z3_ast inst) {
|
||||
// ctx: user context data
|
||||
// cb: solver callback handle (internal use)
|
||||
// q: quantifier being instantiated
|
||||
// inst: proposed instantiation
|
||||
|
||||
// Your logic here
|
||||
return Z3_TRUE; // or Z3_FALSE to block
|
||||
}
|
||||
|
||||
// Register the callback
|
||||
Z3_context ctx = Z3_mk_context(Z3_mk_config());
|
||||
Z3_solver s = Z3_mk_solver(ctx);
|
||||
Z3_solver_propagate_on_binding(ctx, s, my_callback);
|
||||
```
|
||||
|
||||
### C++ API
|
||||
|
||||
The C++ API follows the same pattern as the C API, using the same `Z3_solver_propagate_on_binding` function.
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Example: Limiting Instantiations
|
||||
|
||||
```python
|
||||
class InstantiationLimiter(UserPropagateBase):
|
||||
def __init__(self, max_instantiations=5, s=None, ctx=None):
|
||||
UserPropagateBase.__init__(self, s, ctx)
|
||||
self.max_instantiations = max_instantiations
|
||||
self.count = 0
|
||||
self.add_on_binding(self.limit_instantiations)
|
||||
|
||||
def limit_instantiations(self, quantifier, instantiation):
|
||||
self.count += 1
|
||||
print(f"Instantiation #{self.count}: {instantiation}")
|
||||
|
||||
# Allow only the first max_instantiations
|
||||
if self.count <= self.max_instantiations:
|
||||
print(" -> ALLOWED")
|
||||
return True
|
||||
else:
|
||||
print(" -> BLOCKED")
|
||||
return False
|
||||
|
||||
def push(self): pass
|
||||
def pop(self, num_scopes): pass
|
||||
def fresh(self, new_ctx): return InstantiationLimiter(self.max_instantiations, ctx=new_ctx)
|
||||
|
||||
# Usage
|
||||
s = Solver()
|
||||
limiter = InstantiationLimiter(max_instantiations=3, s=s)
|
||||
|
||||
x = Int('x')
|
||||
f = Function('f', IntSort(), IntSort())
|
||||
s.add(ForAll([x], f(x) >= 0))
|
||||
s.add(f(1) < 10, f(2) < 20, f(3) < 30)
|
||||
|
||||
result = s.check()
|
||||
```
|
||||
|
||||
### Advanced Example: Pattern-Based Filtering
|
||||
|
||||
```python
|
||||
class PatternFilter(UserPropagateBase):
|
||||
def __init__(self, s=None, ctx=None):
|
||||
UserPropagateBase.__init__(self, s, ctx)
|
||||
self.pattern_counts = {}
|
||||
self.max_per_pattern = 2
|
||||
self.add_on_binding(self.filter_by_pattern)
|
||||
|
||||
def filter_by_pattern(self, quantifier, instantiation):
|
||||
# Convert to string for pattern matching
|
||||
pattern = str(instantiation)
|
||||
|
||||
# Count occurrences of this pattern
|
||||
self.pattern_counts[pattern] = self.pattern_counts.get(pattern, 0) + 1
|
||||
count = self.pattern_counts[pattern]
|
||||
|
||||
# Allow only max_per_pattern instantiations of each pattern
|
||||
allow = count <= self.max_per_pattern
|
||||
|
||||
print(f"Pattern: {pattern} (#{count}) -> {'ALLOWED' if allow else 'BLOCKED'}")
|
||||
return allow
|
||||
|
||||
def push(self): pass
|
||||
def pop(self, num_scopes): pass
|
||||
def fresh(self, new_ctx): return PatternFilter(ctx=new_ctx)
|
||||
```
|
||||
|
||||
### Logging Example: Analyzing Instantiation Patterns
|
||||
|
||||
```python
|
||||
class InstantiationLogger(UserPropagateBase):
|
||||
def __init__(self, s=None, ctx=None):
|
||||
UserPropagateBase.__init__(self, s, ctx)
|
||||
self.log = []
|
||||
self.add_on_binding(self.log_instantiation)
|
||||
|
||||
def log_instantiation(self, quantifier, instantiation):
|
||||
entry = {
|
||||
'quantifier': str(quantifier),
|
||||
'instantiation': str(instantiation),
|
||||
'count': len(self.log) + 1
|
||||
}
|
||||
self.log.append(entry)
|
||||
|
||||
print(f"#{entry['count']}: {entry['instantiation']}")
|
||||
|
||||
# Allow all instantiations (just log them)
|
||||
return True
|
||||
|
||||
def get_statistics(self):
|
||||
# Group by quantifier
|
||||
by_quantifier = {}
|
||||
for entry in self.log:
|
||||
q = entry['quantifier']
|
||||
if q not in by_quantifier:
|
||||
by_quantifier[q] = []
|
||||
by_quantifier[q].append(entry['instantiation'])
|
||||
|
||||
return {
|
||||
'total': len(self.log),
|
||||
'by_quantifier': by_quantifier
|
||||
}
|
||||
|
||||
def push(self): pass
|
||||
def pop(self, num_scopes): pass
|
||||
def fresh(self, new_ctx): return InstantiationLogger(ctx=new_ctx)
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Performance Optimization
|
||||
- **Problem**: Some quantifier instantiations are expensive but rarely useful
|
||||
- **Solution**: Use callback to block certain patterns or limit instantiation count
|
||||
- **Benefit**: Reduced solving time, better resource utilization
|
||||
|
||||
### 2. Custom Instantiation Strategies
|
||||
- **Problem**: Default instantiation heuristics don't work well for your domain
|
||||
- **Solution**: Implement domain-specific filtering logic
|
||||
- **Benefit**: Better solution quality, faster convergence
|
||||
|
||||
### 3. Debugging and Analysis
|
||||
- **Problem**: Understanding why a formula is UNSAT or takes long to solve
|
||||
- **Solution**: Log all instantiation attempts to analyze patterns
|
||||
- **Benefit**: Better insight into solver behavior
|
||||
|
||||
### 4. Interactive Solving
|
||||
- **Problem**: Need to control solving process interactively
|
||||
- **Solution**: Use callback to selectively enable/disable instantiations
|
||||
- **Benefit**: Fine-grained control over solver behavior
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Callback Invocation
|
||||
- Called **before** the instantiation is added to the solver
|
||||
- Blocking an instantiation prevents it from being used in the current search
|
||||
- The same instantiation may be proposed again in different search branches
|
||||
|
||||
### Return Value Semantics
|
||||
- `True` (Python) / `Z3_TRUE` (C): Allow the instantiation
|
||||
- `False` (Python) / `Z3_FALSE` (C): Block the instantiation
|
||||
- Blocked instantiations are discarded and won't be used in current search
|
||||
|
||||
### Thread Safety
|
||||
- Callbacks are invoked on the same thread as the solver
|
||||
- No additional synchronization is needed
|
||||
- Context switching during callback execution is safe
|
||||
|
||||
### Performance Considerations
|
||||
- Callbacks are invoked frequently during solving
|
||||
- Keep callback logic lightweight to avoid performance overhead
|
||||
- String conversions (`str(ast)`) can be expensive; cache when possible
|
||||
- Consider using AST structure inspection instead of string matching
|
||||
|
||||
## Limitations
|
||||
|
||||
### C/C++ API Limitations
|
||||
- The C/C++ callback receives AST handles but no direct Z3 context
|
||||
- Converting ASTs to strings for inspection requires careful context management
|
||||
- Recommend using Python API for complex logic, C/C++ for simple filtering
|
||||
|
||||
### Scope and Lifetime
|
||||
- Callback registrations are tied to solver instances
|
||||
- User propagator instances must remain alive during solving
|
||||
- AST handles in callbacks are valid only during callback execution
|
||||
|
||||
### Interaction with Other Features
|
||||
- Works with all quantifier instantiation strategies (E-matching, MBQI, etc.)
|
||||
- Compatible with other user propagator callbacks
|
||||
- May affect solver completeness if too many instantiations are blocked
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start Simple**: Begin with logging to understand instantiation patterns
|
||||
2. **Be Conservative**: Blocking too many instantiations can make formulas unsolvable
|
||||
3. **Test Thoroughly**: Verify that your filtering doesn't break correctness
|
||||
4. **Profile Performance**: Measure impact of callback overhead
|
||||
5. **Use Appropriate Data Structures**: Hash maps for pattern counting, etc.
|
||||
6. **Handle Edge Cases**: Empty instantiations, malformed ASTs, etc.
|
||||
|
||||
## Complete Working Example
|
||||
|
||||
See the accompanying files:
|
||||
- `examples/python/quantifier_instantiation_callback.py` - Complete Python examples
|
||||
- `examples/c++/quantifier_instantiation_callback.cpp` - C++ examples
|
||||
- `examples/c/quantifier_instantiation_callback.c` - C examples
|
||||
|
||||
These examples demonstrate all the concepts above with runnable code.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Callback Not Called**
|
||||
- Ensure user propagator is properly registered with solver
|
||||
- Check that problem actually triggers quantifier instantiations
|
||||
- Verify quantifier syntax is correct
|
||||
|
||||
2. **Performance Degradation**
|
||||
- Simplify callback logic
|
||||
- Avoid expensive string operations
|
||||
- Consider sampling (only process every Nth callback)
|
||||
|
||||
3. **Unexpected UNSAT Results**
|
||||
- Review blocking criteria - may be too aggressive
|
||||
- Test with callback disabled to verify baseline behavior
|
||||
- Use logging to understand what's being blocked
|
||||
|
||||
4. **Memory Issues**
|
||||
- Don't store AST handles beyond callback lifetime
|
||||
- Clear large data structures periodically
|
||||
- Monitor memory usage in long-running processes
|
||||
|
||||
## Version History
|
||||
|
||||
- **Z3 4.15.3**: Initial implementation of quantifier instantiation callbacks
|
||||
- **Z3 4.15.4**: Improved performance and stability
|
||||
|
||||
For more information, see the Z3 documentation and source code in the Z3Prover repository.
|
Loading…
Add table
Add a link
Reference in a new issue