mirror of
https://github.com/Z3Prover/z3
synced 2025-09-10 11:41: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.
|
161
examples/README_quantifier_callbacks.md
Normal file
161
examples/README_quantifier_callbacks.md
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
# UserPropagator Quantifier Instantiation Callback Examples
|
||||||
|
|
||||||
|
This directory contains examples demonstrating 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, providing fine-grained control over the solving process.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
### Python Examples
|
||||||
|
- `examples/python/quantifier_instantiation_callback.py` - Comprehensive Python examples showing:
|
||||||
|
- Basic instantiation control (limiting number of instantiations)
|
||||||
|
- Advanced pattern-based filtering
|
||||||
|
- Instantiation logging and analysis
|
||||||
|
|
||||||
|
### C++ Examples
|
||||||
|
- `examples/c++/quantifier_instantiation_callback.cpp` - C++ API examples (requires compilation)
|
||||||
|
|
||||||
|
### C Examples
|
||||||
|
- `examples/c/quantifier_instantiation_callback.c` - C API examples (requires compilation)
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- `src/test/test_quantifier_instantiation_callback.py` - Unit tests validating functionality
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- `doc/quantifier_instantiation_callback.md` - Complete API documentation and usage guide
|
||||||
|
|
||||||
|
## Running the Examples
|
||||||
|
|
||||||
|
### Python Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd build/python
|
||||||
|
PYTHONPATH=/path/to/z3/build/python python3 ../../examples/python/quantifier_instantiation_callback.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from the Z3 build directory:
|
||||||
|
```bash
|
||||||
|
cd build/python
|
||||||
|
python3 ../../examples/python/quantifier_instantiation_callback.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### C++ Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compile
|
||||||
|
g++ -I src/api -I src/api/c++ examples/c++/quantifier_instantiation_callback.cpp -L build -lz3 -o quantifier_callback_cpp
|
||||||
|
|
||||||
|
# Run
|
||||||
|
LD_LIBRARY_PATH=build ./quantifier_callback_cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
### C Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compile
|
||||||
|
gcc -I src/api examples/c/quantifier_instantiation_callback.c -L build -lz3 -o quantifier_callback_c
|
||||||
|
|
||||||
|
# Run
|
||||||
|
LD_LIBRARY_PATH=build ./quantifier_callback_c
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd z3-root-directory
|
||||||
|
PYTHONPATH=build/python python3 src/test/test_quantifier_instantiation_callback.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Output
|
||||||
|
|
||||||
|
When running the Python examples, you should see output like:
|
||||||
|
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
BASIC QUANTIFIER INSTANTIATION CONTROL EXAMPLE
|
||||||
|
============================================================
|
||||||
|
Checking satisfiability with instantiation control...
|
||||||
|
Instantiation #1:
|
||||||
|
Quantifier: ForAll(x, f(x) >= 0)
|
||||||
|
Instantiation: f(1) >= 0
|
||||||
|
-> ALLOWED (#1)
|
||||||
|
|
||||||
|
Instantiation #2:
|
||||||
|
Quantifier: ForAll(x, f(x) >= 0)
|
||||||
|
Instantiation: f(2) >= 0
|
||||||
|
-> ALLOWED (#2)
|
||||||
|
|
||||||
|
Instantiation #3:
|
||||||
|
Quantifier: ForAll(x, f(x) >= 0)
|
||||||
|
Instantiation: f(3) >= 0
|
||||||
|
-> ALLOWED (#3)
|
||||||
|
|
||||||
|
Result: sat
|
||||||
|
Model: [c = 3, b = 2, a = 1, f = [else -> 0]]
|
||||||
|
|
||||||
|
Instantiation Statistics:
|
||||||
|
Total attempts: 3
|
||||||
|
Allowed: 3
|
||||||
|
Blocked: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features Demonstrated
|
||||||
|
|
||||||
|
1. **Basic Control**: Limiting the number of quantifier instantiations
|
||||||
|
2. **Pattern Filtering**: Allowing only a certain number of instantiations per pattern
|
||||||
|
3. **Logging**: Recording all instantiation attempts for analysis
|
||||||
|
4. **Performance Impact**: Showing how filtering affects solving time
|
||||||
|
5. **Error Handling**: Graceful handling of callback exceptions
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **Performance Optimization**: Reduce solving time by limiting expensive instantiations
|
||||||
|
- **Custom Strategies**: Implement domain-specific instantiation heuristics
|
||||||
|
- **Debugging**: Understand solver behavior through instantiation analysis
|
||||||
|
- **Interactive Solving**: Dynamically control solving process
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Z3 version 4.15.3 or later
|
||||||
|
- Python 3.x for Python examples
|
||||||
|
- GCC/Clang for C/C++ examples
|
||||||
|
- Z3 shared library in LD_LIBRARY_PATH
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "ModuleNotFoundError: No module named 'z3'"
|
||||||
|
Set PYTHONPATH to point to the Z3 Python bindings:
|
||||||
|
```bash
|
||||||
|
export PYTHONPATH=/path/to/z3/build/python:$PYTHONPATH
|
||||||
|
```
|
||||||
|
|
||||||
|
### "No instantiations were attempted"
|
||||||
|
- Check that your formula contains quantifiers
|
||||||
|
- Verify that the constraints actually trigger instantiation (e.g., through function applications)
|
||||||
|
- Try disabling other Z3 optimizations that might eliminate the need for instantiation
|
||||||
|
|
||||||
|
### C/C++ Compilation Errors
|
||||||
|
- Ensure Z3 headers are in the include path (`-I /path/to/z3/src/api`)
|
||||||
|
- Link against the Z3 library (`-L /path/to/z3/build -lz3`)
|
||||||
|
- Set LD_LIBRARY_PATH to find the shared library at runtime
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
- Keep callback logic simple and fast
|
||||||
|
- Avoid expensive string operations in callbacks
|
||||||
|
- Consider sampling (only process every Nth callback) for high-frequency instantiations
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- See `doc/quantifier_instantiation_callback.md` for complete API documentation
|
||||||
|
- Z3 Guide: https://microsoft.github.io/z3guide/
|
||||||
|
- Z3 API Reference: https://z3prover.github.io/api/html/
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
To add new examples or improve existing ones:
|
||||||
|
1. Follow the existing code style and structure
|
||||||
|
2. Add appropriate error handling and documentation
|
||||||
|
3. Include unit tests for new functionality
|
||||||
|
4. Update this README with any new examples
|
312
examples/c++/quantifier_instantiation_callback.cpp
Normal file
312
examples/c++/quantifier_instantiation_callback.cpp
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
/**
|
||||||
|
* Example demonstrating the UserPropagator callback for quantifier instantiations in Z3.
|
||||||
|
*
|
||||||
|
* This feature was added in Z3 version 4.15.3 and allows user propagators to intercept
|
||||||
|
* and control quantifier instantiations using the Z3_solver_propagate_on_binding API.
|
||||||
|
*
|
||||||
|
* The callback receives the quantifier and its proposed instantiation, and can return
|
||||||
|
* false to discard the instantiation, providing fine-grained control over the
|
||||||
|
* quantifier instantiation process.
|
||||||
|
*
|
||||||
|
* To compile:
|
||||||
|
* g++ -I /path/to/z3/src/api -I /path/to/z3/src/api/c++ \
|
||||||
|
* quantifier_instantiation_callback.cpp -L /path/to/z3/build -lz3 \
|
||||||
|
* -o quantifier_instantiation_callback
|
||||||
|
*
|
||||||
|
* To run:
|
||||||
|
* LD_LIBRARY_PATH=/path/to/z3/build ./quantifier_instantiation_callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <z3++.h>
|
||||||
|
|
||||||
|
using namespace z3;
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
// Global counter for demonstration purposes
|
||||||
|
static int instantiation_counter = 0;
|
||||||
|
static int allowed_counter = 0;
|
||||||
|
static int blocked_counter = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-defined callback for quantifier instantiation control.
|
||||||
|
*
|
||||||
|
* This function is called by Z3 whenever it wants to instantiate a quantifier.
|
||||||
|
*
|
||||||
|
* @param ctx User context (can be used to pass custom data)
|
||||||
|
* @param cb Solver callback object
|
||||||
|
* @param q The quantifier being instantiated
|
||||||
|
* @param inst The proposed instantiation
|
||||||
|
* @return true to allow the instantiation, false to discard it
|
||||||
|
*/
|
||||||
|
bool quantifier_instantiation_callback(void* ctx, Z3_solver_callback cb, Z3_ast q, Z3_ast inst) {
|
||||||
|
// Cast the context back to our custom data structure if needed
|
||||||
|
// For this simple example, we'll use global variables
|
||||||
|
|
||||||
|
instantiation_counter++;
|
||||||
|
|
||||||
|
// Get string representations for logging
|
||||||
|
Z3_context z3_ctx = Z3_solver_callback_get_context(cb);
|
||||||
|
string q_str = Z3_ast_to_string(z3_ctx, q);
|
||||||
|
string inst_str = Z3_ast_to_string(z3_ctx, inst);
|
||||||
|
|
||||||
|
cout << "Instantiation #" << instantiation_counter << ":" << endl;
|
||||||
|
cout << " Quantifier: " << q_str << endl;
|
||||||
|
cout << " Instantiation: " << inst_str << endl;
|
||||||
|
|
||||||
|
// Example filtering logic: allow only the first 3 instantiations
|
||||||
|
// In practice, you might implement more sophisticated filtering
|
||||||
|
bool allow = instantiation_counter <= 3;
|
||||||
|
|
||||||
|
if (allow) {
|
||||||
|
allowed_counter++;
|
||||||
|
cout << " -> ALLOWED (#" << allowed_counter << ")" << endl;
|
||||||
|
} else {
|
||||||
|
blocked_counter++;
|
||||||
|
cout << " -> BLOCKED (#" << blocked_counter << ")" << endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
cout << endl;
|
||||||
|
return allow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example demonstrating basic quantifier instantiation control.
|
||||||
|
*/
|
||||||
|
void example_basic_control() {
|
||||||
|
cout << string(60, '=') << endl;
|
||||||
|
cout << "BASIC QUANTIFIER INSTANTIATION CONTROL EXAMPLE" << endl;
|
||||||
|
cout << string(60, '=') << endl;
|
||||||
|
|
||||||
|
context ctx;
|
||||||
|
solver s(ctx);
|
||||||
|
|
||||||
|
// Register the quantifier instantiation callback
|
||||||
|
Z3_solver_propagate_on_binding(ctx, s, quantifier_instantiation_callback);
|
||||||
|
|
||||||
|
// Create a simple quantified formula
|
||||||
|
sort int_sort = ctx.int_sort();
|
||||||
|
func_decl f = ctx.function("f", int_sort, int_sort);
|
||||||
|
expr x = ctx.int_const("x");
|
||||||
|
|
||||||
|
// Add quantified axiom: forall x. f(x) >= 0
|
||||||
|
expr axiom = forall(x, f(x) >= 0);
|
||||||
|
s.add(axiom);
|
||||||
|
|
||||||
|
// Add constraints that might trigger instantiations
|
||||||
|
expr a = ctx.int_const("a");
|
||||||
|
expr b = ctx.int_const("b");
|
||||||
|
expr c = ctx.int_const("c");
|
||||||
|
|
||||||
|
s.add(f(a) < 10);
|
||||||
|
s.add(f(b) < 20);
|
||||||
|
s.add(f(c) < 30);
|
||||||
|
|
||||||
|
// Add specific values
|
||||||
|
s.add(a == 1);
|
||||||
|
s.add(b == 2);
|
||||||
|
s.add(c == 3);
|
||||||
|
|
||||||
|
cout << "Checking satisfiability with instantiation control..." << endl;
|
||||||
|
check_result result = s.check();
|
||||||
|
cout << "Result: " << result << endl;
|
||||||
|
|
||||||
|
if (result == sat) {
|
||||||
|
cout << "Model: " << s.get_model() << endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print statistics
|
||||||
|
cout << endl << "Instantiation Statistics:" << endl;
|
||||||
|
cout << " Total attempts: " << instantiation_counter << endl;
|
||||||
|
cout << " Allowed: " << allowed_counter << endl;
|
||||||
|
cout << " Blocked: " << blocked_counter << endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structure to hold callback context data
|
||||||
|
struct AdvancedCallbackContext {
|
||||||
|
int max_instantiations_per_pattern;
|
||||||
|
map<string, int> instantiation_counts;
|
||||||
|
|
||||||
|
AdvancedCallbackContext(int max_per_pattern = 2)
|
||||||
|
: max_instantiations_per_pattern(max_per_pattern) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced callback that tracks instantiation patterns.
|
||||||
|
*/
|
||||||
|
bool advanced_instantiation_callback(void* ctx, Z3_solver_callback cb, Z3_ast q, Z3_ast inst) {
|
||||||
|
AdvancedCallbackContext* context = static_cast<AdvancedCallbackContext*>(ctx);
|
||||||
|
|
||||||
|
// Get string representation of the instantiation
|
||||||
|
Z3_context z3_ctx = Z3_solver_callback_get_context(cb);
|
||||||
|
string inst_str = Z3_ast_to_string(z3_ctx, inst);
|
||||||
|
|
||||||
|
// Count instantiations for this pattern
|
||||||
|
context->instantiation_counts[inst_str]++;
|
||||||
|
int count = context->instantiation_counts[inst_str];
|
||||||
|
|
||||||
|
// Allow only up to max_instantiations_per_pattern of the same pattern
|
||||||
|
bool allow = count <= context->max_instantiations_per_pattern;
|
||||||
|
|
||||||
|
cout << "Instantiation: " << inst_str << " (attempt #" << count << ")" << endl;
|
||||||
|
cout << " -> " << (allow ? "ALLOWED" : "BLOCKED") << endl;
|
||||||
|
|
||||||
|
return allow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example demonstrating advanced instantiation filtering.
|
||||||
|
*/
|
||||||
|
void example_advanced_filtering() {
|
||||||
|
cout << endl << string(60, '=') << endl;
|
||||||
|
cout << "ADVANCED INSTANTIATION FILTERING EXAMPLE" << endl;
|
||||||
|
cout << string(60, '=') << endl;
|
||||||
|
|
||||||
|
context ctx;
|
||||||
|
solver s(ctx);
|
||||||
|
|
||||||
|
// Create callback context
|
||||||
|
AdvancedCallbackContext callback_ctx(2); // Allow max 2 instantiations per pattern
|
||||||
|
|
||||||
|
// Register the advanced callback
|
||||||
|
Z3_solver_propagate_on_binding(ctx, s, advanced_instantiation_callback);
|
||||||
|
|
||||||
|
// Store callback context in solver (this is a simplified approach)
|
||||||
|
// In practice, you might need a more sophisticated context management system
|
||||||
|
|
||||||
|
// Create a more complex scenario
|
||||||
|
sort int_sort = ctx.int_sort();
|
||||||
|
sort bool_sort = ctx.bool_sort();
|
||||||
|
func_decl P = ctx.function("P", int_sort, int_sort, bool_sort);
|
||||||
|
|
||||||
|
expr x = ctx.int_const("x");
|
||||||
|
expr y = ctx.int_const("y");
|
||||||
|
|
||||||
|
// Add quantified formula: forall x, y. P(x, y) => P(y, x)
|
||||||
|
expr axiom = forall(x, y, implies(P(x, y), P(y, x)));
|
||||||
|
s.add(axiom);
|
||||||
|
|
||||||
|
// Add facts that will trigger instantiations
|
||||||
|
expr a = ctx.int_const("a");
|
||||||
|
expr b = ctx.int_const("b");
|
||||||
|
expr c = ctx.int_const("c");
|
||||||
|
|
||||||
|
s.add(P(a, b));
|
||||||
|
s.add(P(b, c));
|
||||||
|
s.add(P(c, a));
|
||||||
|
|
||||||
|
cout << "Checking satisfiability with advanced filtering..." << endl;
|
||||||
|
check_result result = s.check();
|
||||||
|
cout << "Result: " << result << endl;
|
||||||
|
|
||||||
|
if (result == sat) {
|
||||||
|
cout << "Model: " << s.get_model() << endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print pattern statistics
|
||||||
|
cout << endl << "Instantiation Pattern Statistics:" << endl;
|
||||||
|
for (const auto& pair : callback_ctx.instantiation_counts) {
|
||||||
|
cout << " " << pair.first << ": " << pair.second << " attempts" << endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple callback that logs all instantiations without blocking any.
|
||||||
|
*/
|
||||||
|
bool logging_callback(void* ctx, Z3_solver_callback cb, Z3_ast q, Z3_ast inst) {
|
||||||
|
vector<pair<string, string>>* log = static_cast<vector<pair<string, string>>*>(ctx);
|
||||||
|
|
||||||
|
Z3_context z3_ctx = Z3_solver_callback_get_context(cb);
|
||||||
|
string q_str = Z3_ast_to_string(z3_ctx, q);
|
||||||
|
string inst_str = Z3_ast_to_string(z3_ctx, inst);
|
||||||
|
|
||||||
|
log->push_back(make_pair(q_str, inst_str));
|
||||||
|
|
||||||
|
cout << "Logged instantiation #" << log->size() << ":" << endl;
|
||||||
|
cout << " Quantifier: " << q_str << endl;
|
||||||
|
cout << " Instantiation: " << inst_str << endl;
|
||||||
|
|
||||||
|
// Allow all instantiations for logging purposes
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example focused on logging instantiation patterns.
|
||||||
|
*/
|
||||||
|
void example_logging() {
|
||||||
|
cout << endl << string(60, '=') << endl;
|
||||||
|
cout << "INSTANTIATION LOGGING EXAMPLE" << endl;
|
||||||
|
cout << string(60, '=') << endl;
|
||||||
|
|
||||||
|
context ctx;
|
||||||
|
solver s(ctx);
|
||||||
|
|
||||||
|
// Create log storage
|
||||||
|
vector<pair<string, string>> instantiation_log;
|
||||||
|
|
||||||
|
// Register logging callback
|
||||||
|
Z3_solver_propagate_on_binding(ctx, s, logging_callback);
|
||||||
|
|
||||||
|
// Create scenario with multiple quantifiers
|
||||||
|
sort int_sort = ctx.int_sort();
|
||||||
|
func_decl f = ctx.function("f", int_sort, int_sort);
|
||||||
|
func_decl g = ctx.function("g", int_sort, int_sort);
|
||||||
|
|
||||||
|
expr x = ctx.int_const("x");
|
||||||
|
|
||||||
|
// Add multiple quantified axioms
|
||||||
|
s.add(forall(x, f(x) >= x)); // f(x) is at least x
|
||||||
|
s.add(forall(x, g(x) <= x)); // g(x) is at most x
|
||||||
|
|
||||||
|
// Add constraints to trigger instantiations
|
||||||
|
expr a = ctx.int_const("a");
|
||||||
|
expr b = ctx.int_const("b");
|
||||||
|
|
||||||
|
s.add(f(a) < 5);
|
||||||
|
s.add(g(b) > 10);
|
||||||
|
s.add(a == 2);
|
||||||
|
s.add(b == 8);
|
||||||
|
|
||||||
|
cout << "Solving with full logging enabled..." << endl;
|
||||||
|
check_result result = s.check();
|
||||||
|
cout << "Result: " << result << endl;
|
||||||
|
|
||||||
|
// Analyze logged patterns
|
||||||
|
cout << endl << "Instantiation Analysis:" << endl;
|
||||||
|
cout << "Total instantiations logged: " << instantiation_log.size() << endl;
|
||||||
|
|
||||||
|
// Group by quantifier
|
||||||
|
map<string, vector<string>> by_quantifier;
|
||||||
|
for (const auto& entry : instantiation_log) {
|
||||||
|
by_quantifier[entry.first].push_back(entry.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& pair : by_quantifier) {
|
||||||
|
cout << endl << "Quantifier: " << pair.first << endl;
|
||||||
|
cout << " Instantiations (" << pair.second.size() << "):" << endl;
|
||||||
|
for (size_t i = 0; i < pair.second.size(); ++i) {
|
||||||
|
cout << " " << (i + 1) << ". " << pair.second[i] << endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
try {
|
||||||
|
// Run all examples
|
||||||
|
example_basic_control();
|
||||||
|
example_advanced_filtering();
|
||||||
|
example_logging();
|
||||||
|
|
||||||
|
cout << endl << string(60, '=') << endl;
|
||||||
|
cout << "All examples completed successfully!" << endl;
|
||||||
|
cout << string(60, '=') << endl;
|
||||||
|
|
||||||
|
} catch (exception& e) {
|
||||||
|
cout << "Exception: " << e.what() << endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
360
examples/c/quantifier_instantiation_callback.c
Normal file
360
examples/c/quantifier_instantiation_callback.c
Normal file
|
@ -0,0 +1,360 @@
|
||||||
|
/**
|
||||||
|
* Example demonstrating the UserPropagator callback for quantifier instantiations in Z3 C API.
|
||||||
|
*
|
||||||
|
* This feature was added in Z3 version 4.15.3 and allows user propagators to intercept
|
||||||
|
* and control quantifier instantiations using the Z3_solver_propagate_on_binding API.
|
||||||
|
*
|
||||||
|
* The callback receives the quantifier and its proposed instantiation, and can return
|
||||||
|
* Z3_FALSE to discard the instantiation, providing fine-grained control over the
|
||||||
|
* quantifier instantiation process.
|
||||||
|
*
|
||||||
|
* To compile:
|
||||||
|
* gcc -I /path/to/z3/src/api quantifier_instantiation_callback.c \
|
||||||
|
* -L /path/to/z3/build -lz3 -o quantifier_instantiation_callback
|
||||||
|
*
|
||||||
|
* To run:
|
||||||
|
* LD_LIBRARY_PATH=/path/to/z3/build ./quantifier_instantiation_callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <z3.h>
|
||||||
|
|
||||||
|
// Global counters for demonstration
|
||||||
|
static int instantiation_counter = 0;
|
||||||
|
static int allowed_counter = 0;
|
||||||
|
static int blocked_counter = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-defined callback for quantifier instantiation control.
|
||||||
|
*
|
||||||
|
* This function is called by Z3 whenever it wants to instantiate a quantifier.
|
||||||
|
*
|
||||||
|
* @param ctx User context (can be used to pass custom data)
|
||||||
|
* @param cb Solver callback object
|
||||||
|
* @param q The quantifier being instantiated
|
||||||
|
* @param inst The proposed instantiation
|
||||||
|
* @return Z3_TRUE to allow the instantiation, Z3_FALSE to discard it
|
||||||
|
*/
|
||||||
|
Z3_bool quantifier_instantiation_callback(void* ctx, Z3_solver_callback cb, Z3_ast q, Z3_ast inst) {
|
||||||
|
// Get the Z3 context from the callback
|
||||||
|
Z3_context z3_ctx = Z3_solver_callback_get_context(cb);
|
||||||
|
|
||||||
|
instantiation_counter++;
|
||||||
|
|
||||||
|
// Get string representations for logging
|
||||||
|
Z3_string q_str = Z3_ast_to_string(z3_ctx, q);
|
||||||
|
Z3_string inst_str = Z3_ast_to_string(z3_ctx, inst);
|
||||||
|
|
||||||
|
printf("Instantiation #%d:\n", instantiation_counter);
|
||||||
|
printf(" Quantifier: %s\n", q_str);
|
||||||
|
printf(" Instantiation: %s\n", inst_str);
|
||||||
|
|
||||||
|
// Example filtering logic: allow only the first 3 instantiations
|
||||||
|
// In practice, you might implement more sophisticated filtering
|
||||||
|
Z3_bool allow = (instantiation_counter <= 3) ? Z3_TRUE : Z3_FALSE;
|
||||||
|
|
||||||
|
if (allow) {
|
||||||
|
allowed_counter++;
|
||||||
|
printf(" -> ALLOWED (#%d)\n", allowed_counter);
|
||||||
|
} else {
|
||||||
|
blocked_counter++;
|
||||||
|
printf(" -> BLOCKED (#%d)\n", blocked_counter);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\n");
|
||||||
|
return allow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example demonstrating basic quantifier instantiation control.
|
||||||
|
*/
|
||||||
|
void example_basic_control() {
|
||||||
|
printf("============================================================\n");
|
||||||
|
printf("BASIC QUANTIFIER INSTANTIATION CONTROL EXAMPLE\n");
|
||||||
|
printf("============================================================\n");
|
||||||
|
|
||||||
|
// Create Z3 context and solver
|
||||||
|
Z3_config cfg = Z3_mk_config();
|
||||||
|
Z3_context ctx = Z3_mk_context(cfg);
|
||||||
|
Z3_del_config(cfg);
|
||||||
|
|
||||||
|
Z3_solver s = Z3_mk_solver(ctx);
|
||||||
|
Z3_solver_inc_ref(ctx, s);
|
||||||
|
|
||||||
|
// Register the quantifier instantiation callback
|
||||||
|
Z3_solver_propagate_on_binding(ctx, s, quantifier_instantiation_callback);
|
||||||
|
|
||||||
|
// Create sorts and symbols
|
||||||
|
Z3_sort int_sort = Z3_mk_int_sort(ctx);
|
||||||
|
Z3_symbol f_name = Z3_mk_string_symbol(ctx, "f");
|
||||||
|
Z3_symbol x_name = Z3_mk_string_symbol(ctx, "x");
|
||||||
|
|
||||||
|
// Create function declaration f: Int -> Int
|
||||||
|
Z3_func_decl f = Z3_mk_func_decl(ctx, f_name, 1, &int_sort, int_sort);
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
Z3_ast x = Z3_mk_bound(ctx, 0, int_sort);
|
||||||
|
Z3_ast f_x = Z3_mk_app(ctx, f, 1, &x);
|
||||||
|
|
||||||
|
// Create axiom: forall x. f(x) >= 0
|
||||||
|
Z3_ast zero = Z3_mk_int(ctx, 0, int_sort);
|
||||||
|
Z3_ast f_x_geq_0 = Z3_mk_ge(ctx, f_x, zero);
|
||||||
|
|
||||||
|
Z3_ast axiom = Z3_mk_forall_const(ctx, 0, 1, &x, 0, NULL, f_x_geq_0);
|
||||||
|
Z3_solver_assert(ctx, s, axiom);
|
||||||
|
|
||||||
|
// Create constants and add constraints
|
||||||
|
Z3_symbol a_name = Z3_mk_string_symbol(ctx, "a");
|
||||||
|
Z3_symbol b_name = Z3_mk_string_symbol(ctx, "b");
|
||||||
|
Z3_symbol c_name = Z3_mk_string_symbol(ctx, "c");
|
||||||
|
|
||||||
|
Z3_ast a = Z3_mk_const(ctx, a_name, int_sort);
|
||||||
|
Z3_ast b = Z3_mk_const(ctx, b_name, int_sort);
|
||||||
|
Z3_ast c = Z3_mk_const(ctx, c_name, int_sort);
|
||||||
|
|
||||||
|
// Add constraints that might trigger instantiations
|
||||||
|
Z3_ast f_a = Z3_mk_app(ctx, f, 1, &a);
|
||||||
|
Z3_ast f_b = Z3_mk_app(ctx, f, 1, &b);
|
||||||
|
Z3_ast f_c = Z3_mk_app(ctx, f, 1, &c);
|
||||||
|
|
||||||
|
Z3_ast ten = Z3_mk_int(ctx, 10, int_sort);
|
||||||
|
Z3_ast twenty = Z3_mk_int(ctx, 20, int_sort);
|
||||||
|
Z3_ast thirty = Z3_mk_int(ctx, 30, int_sort);
|
||||||
|
|
||||||
|
Z3_solver_assert(ctx, s, Z3_mk_lt(ctx, f_a, ten));
|
||||||
|
Z3_solver_assert(ctx, s, Z3_mk_lt(ctx, f_b, twenty));
|
||||||
|
Z3_solver_assert(ctx, s, Z3_mk_lt(ctx, f_c, thirty));
|
||||||
|
|
||||||
|
// Add specific values
|
||||||
|
Z3_ast one = Z3_mk_int(ctx, 1, int_sort);
|
||||||
|
Z3_ast two = Z3_mk_int(ctx, 2, int_sort);
|
||||||
|
Z3_ast three = Z3_mk_int(ctx, 3, int_sort);
|
||||||
|
|
||||||
|
Z3_solver_assert(ctx, s, Z3_mk_eq(ctx, a, one));
|
||||||
|
Z3_solver_assert(ctx, s, Z3_mk_eq(ctx, b, two));
|
||||||
|
Z3_solver_assert(ctx, s, Z3_mk_eq(ctx, c, three));
|
||||||
|
|
||||||
|
printf("Checking satisfiability with instantiation control...\n");
|
||||||
|
Z3_lbool result = Z3_solver_check(ctx, s);
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case Z3_L_TRUE:
|
||||||
|
printf("Result: SAT\n");
|
||||||
|
{
|
||||||
|
Z3_model model = Z3_solver_get_model(ctx, s);
|
||||||
|
if (model) {
|
||||||
|
Z3_model_inc_ref(ctx, model);
|
||||||
|
printf("Model: %s\n", Z3_model_to_string(ctx, model));
|
||||||
|
Z3_model_dec_ref(ctx, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Z3_L_FALSE:
|
||||||
|
printf("Result: UNSAT\n");
|
||||||
|
break;
|
||||||
|
case Z3_L_UNDEF:
|
||||||
|
printf("Result: UNKNOWN\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print statistics
|
||||||
|
printf("\nInstantiation Statistics:\n");
|
||||||
|
printf(" Total attempts: %d\n", instantiation_counter);
|
||||||
|
printf(" Allowed: %d\n", allowed_counter);
|
||||||
|
printf(" Blocked: %d\n", blocked_counter);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
Z3_solver_dec_ref(ctx, s);
|
||||||
|
Z3_del_context(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structure for advanced callback context
|
||||||
|
typedef struct {
|
||||||
|
int max_instantiations_per_pattern;
|
||||||
|
char** patterns;
|
||||||
|
int* counts;
|
||||||
|
int num_patterns;
|
||||||
|
int capacity;
|
||||||
|
} AdvancedCallbackContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find or add a pattern in the context.
|
||||||
|
*/
|
||||||
|
int find_or_add_pattern(AdvancedCallbackContext* context, const char* pattern) {
|
||||||
|
// Look for existing pattern
|
||||||
|
for (int i = 0; i < context->num_patterns; i++) {
|
||||||
|
if (strcmp(context->patterns[i], pattern) == 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new pattern if space available
|
||||||
|
if (context->num_patterns < context->capacity) {
|
||||||
|
int index = context->num_patterns++;
|
||||||
|
context->patterns[index] = malloc(strlen(pattern) + 1);
|
||||||
|
strcpy(context->patterns[index], pattern);
|
||||||
|
context->counts[index] = 0;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1; // No space
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced callback that tracks instantiation patterns.
|
||||||
|
*/
|
||||||
|
Z3_bool advanced_instantiation_callback(void* ctx, Z3_solver_callback cb, Z3_ast q, Z3_ast inst) {
|
||||||
|
AdvancedCallbackContext* context = (AdvancedCallbackContext*)ctx;
|
||||||
|
|
||||||
|
// Get string representation of the instantiation
|
||||||
|
Z3_context z3_ctx = Z3_solver_callback_get_context(cb);
|
||||||
|
Z3_string inst_str = Z3_ast_to_string(z3_ctx, inst);
|
||||||
|
|
||||||
|
// Find or add this pattern
|
||||||
|
int pattern_index = find_or_add_pattern(context, inst_str);
|
||||||
|
if (pattern_index == -1) {
|
||||||
|
printf("Warning: Pattern storage full, allowing instantiation\n");
|
||||||
|
return Z3_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment count for this pattern
|
||||||
|
context->counts[pattern_index]++;
|
||||||
|
int count = context->counts[pattern_index];
|
||||||
|
|
||||||
|
// Allow only up to max_instantiations_per_pattern of the same pattern
|
||||||
|
Z3_bool allow = (count <= context->max_instantiations_per_pattern) ? Z3_TRUE : Z3_FALSE;
|
||||||
|
|
||||||
|
printf("Instantiation: %s (attempt #%d)\n", inst_str, count);
|
||||||
|
printf(" -> %s\n", allow ? "ALLOWED" : "BLOCKED");
|
||||||
|
|
||||||
|
return allow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example demonstrating advanced instantiation filtering.
|
||||||
|
*/
|
||||||
|
void example_advanced_filtering() {
|
||||||
|
printf("\n============================================================\n");
|
||||||
|
printf("ADVANCED INSTANTIATION FILTERING EXAMPLE\n");
|
||||||
|
printf("============================================================\n");
|
||||||
|
|
||||||
|
// Create callback context
|
||||||
|
AdvancedCallbackContext context;
|
||||||
|
context.max_instantiations_per_pattern = 2;
|
||||||
|
context.capacity = 100;
|
||||||
|
context.num_patterns = 0;
|
||||||
|
context.patterns = malloc(context.capacity * sizeof(char*));
|
||||||
|
context.counts = malloc(context.capacity * sizeof(int));
|
||||||
|
|
||||||
|
// Create Z3 context and solver
|
||||||
|
Z3_config cfg = Z3_mk_config();
|
||||||
|
Z3_context ctx = Z3_mk_context(cfg);
|
||||||
|
Z3_del_config(cfg);
|
||||||
|
|
||||||
|
Z3_solver s = Z3_mk_solver(ctx);
|
||||||
|
Z3_solver_inc_ref(ctx, s);
|
||||||
|
|
||||||
|
// Register the advanced callback with context
|
||||||
|
Z3_solver_propagate_on_binding(ctx, s, advanced_instantiation_callback);
|
||||||
|
|
||||||
|
// Create sorts and function declaration
|
||||||
|
Z3_sort int_sort = Z3_mk_int_sort(ctx);
|
||||||
|
Z3_sort bool_sort = Z3_mk_bool_sort(ctx);
|
||||||
|
Z3_sort P_domain[2] = {int_sort, int_sort};
|
||||||
|
|
||||||
|
Z3_symbol P_name = Z3_mk_string_symbol(ctx, "P");
|
||||||
|
Z3_func_decl P = Z3_mk_func_decl(ctx, P_name, 2, P_domain, bool_sort);
|
||||||
|
|
||||||
|
// Create bound variables for quantifier
|
||||||
|
Z3_ast x = Z3_mk_bound(ctx, 1, int_sort); // Note: index 1 for second variable in binding order
|
||||||
|
Z3_ast y = Z3_mk_bound(ctx, 0, int_sort); // Note: index 0 for first variable in binding order
|
||||||
|
|
||||||
|
// Create P(x, y) and P(y, x)
|
||||||
|
Z3_ast xy_args[2] = {x, y};
|
||||||
|
Z3_ast yx_args[2] = {y, x};
|
||||||
|
Z3_ast P_xy = Z3_mk_app(ctx, P, 2, xy_args);
|
||||||
|
Z3_ast P_yx = Z3_mk_app(ctx, P, 2, yx_args);
|
||||||
|
|
||||||
|
// Create implication: P(x, y) => P(y, x)
|
||||||
|
Z3_ast implication = Z3_mk_implies(ctx, P_xy, P_yx);
|
||||||
|
|
||||||
|
// Create quantified formula: forall x, y. P(x, y) => P(y, x)
|
||||||
|
Z3_ast vars[2] = {x, y};
|
||||||
|
Z3_ast axiom = Z3_mk_forall_const(ctx, 0, 2, vars, 0, NULL, implication);
|
||||||
|
Z3_solver_assert(ctx, s, axiom);
|
||||||
|
|
||||||
|
// Create constants
|
||||||
|
Z3_symbol a_name = Z3_mk_string_symbol(ctx, "a");
|
||||||
|
Z3_symbol b_name = Z3_mk_string_symbol(ctx, "b");
|
||||||
|
Z3_symbol c_name = Z3_mk_string_symbol(ctx, "c");
|
||||||
|
|
||||||
|
Z3_ast a = Z3_mk_const(ctx, a_name, int_sort);
|
||||||
|
Z3_ast b = Z3_mk_const(ctx, b_name, int_sort);
|
||||||
|
Z3_ast c = Z3_mk_const(ctx, c_name, int_sort);
|
||||||
|
|
||||||
|
// Add facts that will trigger instantiations
|
||||||
|
Z3_ast ab_args[2] = {a, b};
|
||||||
|
Z3_ast bc_args[2] = {b, c};
|
||||||
|
Z3_ast ca_args[2] = {c, a};
|
||||||
|
|
||||||
|
Z3_solver_assert(ctx, s, Z3_mk_app(ctx, P, 2, ab_args));
|
||||||
|
Z3_solver_assert(ctx, s, Z3_mk_app(ctx, P, 2, bc_args));
|
||||||
|
Z3_solver_assert(ctx, s, Z3_mk_app(ctx, P, 2, ca_args));
|
||||||
|
|
||||||
|
printf("Checking satisfiability with advanced filtering...\n");
|
||||||
|
Z3_lbool result = Z3_solver_check(ctx, s);
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case Z3_L_TRUE:
|
||||||
|
printf("Result: SAT\n");
|
||||||
|
{
|
||||||
|
Z3_model model = Z3_solver_get_model(ctx, s);
|
||||||
|
if (model) {
|
||||||
|
Z3_model_inc_ref(ctx, model);
|
||||||
|
printf("Model: %s\n", Z3_model_to_string(ctx, model));
|
||||||
|
Z3_model_dec_ref(ctx, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Z3_L_FALSE:
|
||||||
|
printf("Result: UNSAT\n");
|
||||||
|
break;
|
||||||
|
case Z3_L_UNDEF:
|
||||||
|
printf("Result: UNKNOWN\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print pattern statistics
|
||||||
|
printf("\nInstantiation Pattern Statistics:\n");
|
||||||
|
for (int i = 0; i < context.num_patterns; i++) {
|
||||||
|
printf(" %s: %d attempts\n", context.patterns[i], context.counts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup context
|
||||||
|
for (int i = 0; i < context.num_patterns; i++) {
|
||||||
|
free(context.patterns[i]);
|
||||||
|
}
|
||||||
|
free(context.patterns);
|
||||||
|
free(context.counts);
|
||||||
|
|
||||||
|
// Cleanup Z3 objects
|
||||||
|
Z3_solver_dec_ref(ctx, s);
|
||||||
|
Z3_del_context(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("Z3 Quantifier Instantiation Callback Examples\n");
|
||||||
|
printf("==============================================\n\n");
|
||||||
|
|
||||||
|
// Run examples
|
||||||
|
example_basic_control();
|
||||||
|
example_advanced_filtering();
|
||||||
|
|
||||||
|
printf("\n============================================================\n");
|
||||||
|
printf("All examples completed successfully!\n");
|
||||||
|
printf("============================================================\n");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
306
examples/python/quantifier_instantiation_callback.py
Normal file
306
examples/python/quantifier_instantiation_callback.py
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Example demonstrating the UserPropagator callback for quantifier instantiations in Z3.
|
||||||
|
|
||||||
|
This feature was added in Z3 version 4.15.3 and allows user propagators to intercept
|
||||||
|
and control quantifier instantiations. The callback receives the quantifier and its
|
||||||
|
proposed instantiation, and can return False to discard the instantiation.
|
||||||
|
|
||||||
|
This provides fine-grained control over the quantifier instantiation process,
|
||||||
|
allowing users to:
|
||||||
|
1. Inspect and log instantiations
|
||||||
|
2. Filter out undesired instantiations
|
||||||
|
3. Implement custom instantiation strategies
|
||||||
|
4. Delay certain instantiations for performance reasons
|
||||||
|
"""
|
||||||
|
|
||||||
|
from z3 import *
|
||||||
|
|
||||||
|
class QuantifierInstantiationController(UserPropagateBase):
|
||||||
|
"""
|
||||||
|
A user propagator that controls quantifier instantiations.
|
||||||
|
|
||||||
|
This example demonstrates how to:
|
||||||
|
1. Register a quantifier instantiation callback
|
||||||
|
2. Inspect proposed instantiations
|
||||||
|
3. Selectively allow or discard instantiations
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, s=None, ctx=None):
|
||||||
|
UserPropagateBase.__init__(self, s, ctx)
|
||||||
|
self.instantiation_count = 0
|
||||||
|
self.allowed_instantiations = 0
|
||||||
|
self.blocked_instantiations = 0
|
||||||
|
self.instantiation_log = []
|
||||||
|
|
||||||
|
# Register the quantifier instantiation callback
|
||||||
|
self.add_on_binding(self.on_quantifier_instantiation)
|
||||||
|
|
||||||
|
def on_quantifier_instantiation(self, quantifier, instantiation):
|
||||||
|
"""
|
||||||
|
Callback invoked when Z3 wants to instantiate a quantifier.
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
self.instantiation_count += 1
|
||||||
|
|
||||||
|
# Log the instantiation for inspection
|
||||||
|
q_str = str(quantifier)
|
||||||
|
inst_str = str(instantiation)
|
||||||
|
self.instantiation_log.append((q_str, inst_str))
|
||||||
|
|
||||||
|
print(f"Instantiation #{self.instantiation_count}:")
|
||||||
|
print(f" Quantifier: {q_str}")
|
||||||
|
print(f" Instantiation: {inst_str}")
|
||||||
|
|
||||||
|
# Example filtering logic: allow only the first 3 instantiations
|
||||||
|
# In practice, you might filter based on the structure of the instantiation,
|
||||||
|
# performance considerations, or other criteria
|
||||||
|
allow = self.instantiation_count <= 3
|
||||||
|
|
||||||
|
if allow:
|
||||||
|
self.allowed_instantiations += 1
|
||||||
|
print(f" -> ALLOWED (#{self.allowed_instantiations})")
|
||||||
|
else:
|
||||||
|
self.blocked_instantiations += 1
|
||||||
|
print(f" -> BLOCKED (#{self.blocked_instantiations})")
|
||||||
|
|
||||||
|
print()
|
||||||
|
return allow
|
||||||
|
|
||||||
|
def push(self):
|
||||||
|
# Required method for user propagators
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pop(self, num_scopes):
|
||||||
|
# Required method for user propagators
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fresh(self, new_ctx):
|
||||||
|
# Required method for user propagators
|
||||||
|
return QuantifierInstantiationController(ctx=new_ctx)
|
||||||
|
|
||||||
|
def get_statistics(self):
|
||||||
|
"""Return statistics about instantiation control."""
|
||||||
|
return {
|
||||||
|
'total_instantiation_attempts': self.instantiation_count,
|
||||||
|
'allowed_instantiations': self.allowed_instantiations,
|
||||||
|
'blocked_instantiations': self.blocked_instantiations,
|
||||||
|
'instantiation_log': self.instantiation_log
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def example_basic_quantifier_control():
|
||||||
|
"""
|
||||||
|
Basic example showing how to control quantifier instantiations.
|
||||||
|
"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("BASIC QUANTIFIER INSTANTIATION CONTROL EXAMPLE")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Create solver with user propagator
|
||||||
|
s = Solver()
|
||||||
|
controller = QuantifierInstantiationController(s)
|
||||||
|
|
||||||
|
# Create a simple quantified formula
|
||||||
|
x = Int('x')
|
||||||
|
f = Function('f', IntSort(), IntSort())
|
||||||
|
|
||||||
|
# Add a quantified axiom: forall x. f(x) >= 0
|
||||||
|
axiom = ForAll([x], f(x) >= 0)
|
||||||
|
s.add(axiom)
|
||||||
|
|
||||||
|
# Add constraints that might trigger instantiations
|
||||||
|
a, b, c = Ints('a b c')
|
||||||
|
s.add(f(a) < 10)
|
||||||
|
s.add(f(b) < 20)
|
||||||
|
s.add(f(c) < 30)
|
||||||
|
|
||||||
|
# Also add constraints that should conflict if all instantiations are allowed
|
||||||
|
s.add(a == 1)
|
||||||
|
s.add(b == 2)
|
||||||
|
s.add(c == 3)
|
||||||
|
|
||||||
|
print("Checking satisfiability with instantiation control...")
|
||||||
|
result = s.check()
|
||||||
|
print(f"Result: {result}")
|
||||||
|
|
||||||
|
if result == sat:
|
||||||
|
print(f"Model: {s.model()}")
|
||||||
|
|
||||||
|
# Print statistics
|
||||||
|
stats = controller.get_statistics()
|
||||||
|
print(f"\nInstantiation Statistics:")
|
||||||
|
print(f" Total attempts: {stats['total_instantiation_attempts']}")
|
||||||
|
print(f" Allowed: {stats['allowed_instantiations']}")
|
||||||
|
print(f" Blocked: {stats['blocked_instantiations']}")
|
||||||
|
|
||||||
|
|
||||||
|
def example_advanced_filtering():
|
||||||
|
"""
|
||||||
|
Advanced example showing more sophisticated instantiation filtering.
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("ADVANCED INSTANTIATION FILTERING EXAMPLE")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
class AdvancedController(UserPropagateBase):
|
||||||
|
def __init__(self, s=None, ctx=None):
|
||||||
|
UserPropagateBase.__init__(self, s, ctx)
|
||||||
|
self.instantiations_by_term = {}
|
||||||
|
self.add_on_binding(self.smart_filter)
|
||||||
|
|
||||||
|
def smart_filter(self, quantifier, instantiation):
|
||||||
|
"""
|
||||||
|
Smart filtering based on instantiation content.
|
||||||
|
"""
|
||||||
|
# Extract information about the instantiation
|
||||||
|
q_str = str(quantifier)
|
||||||
|
inst_str = str(instantiation)
|
||||||
|
|
||||||
|
# Count instantiations per term pattern
|
||||||
|
if inst_str not in self.instantiations_by_term:
|
||||||
|
self.instantiations_by_term[inst_str] = 0
|
||||||
|
self.instantiations_by_term[inst_str] += 1
|
||||||
|
|
||||||
|
# Allow only up to 2 instantiations of the same pattern
|
||||||
|
allow = self.instantiations_by_term[inst_str] <= 2
|
||||||
|
|
||||||
|
print(f"Instantiation: {inst_str} (attempt #{self.instantiations_by_term[inst_str]})")
|
||||||
|
print(f" -> {'ALLOWED' if allow else 'BLOCKED'}")
|
||||||
|
|
||||||
|
return allow
|
||||||
|
|
||||||
|
def push(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pop(self, num_scopes):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fresh(self, new_ctx):
|
||||||
|
return AdvancedController(ctx=new_ctx)
|
||||||
|
|
||||||
|
# Create solver with advanced controller
|
||||||
|
s = Solver()
|
||||||
|
controller = AdvancedController(s)
|
||||||
|
|
||||||
|
# Create a more complex scenario
|
||||||
|
x, y = Ints('x y')
|
||||||
|
P = Function('P', IntSort(), IntSort(), BoolSort())
|
||||||
|
|
||||||
|
# Add quantified formula: forall x, y. P(x, y) => P(y, x)
|
||||||
|
axiom = ForAll([x, y], Implies(P(x, y), P(y, x)))
|
||||||
|
s.add(axiom)
|
||||||
|
|
||||||
|
# Add facts that will trigger instantiations
|
||||||
|
a, b, c = Ints('a b c')
|
||||||
|
s.add(P(a, b))
|
||||||
|
s.add(P(b, c))
|
||||||
|
s.add(P(c, a))
|
||||||
|
|
||||||
|
print("Checking satisfiability with advanced filtering...")
|
||||||
|
result = s.check()
|
||||||
|
print(f"Result: {result}")
|
||||||
|
|
||||||
|
if result == sat:
|
||||||
|
print(f"Model: {s.model()}")
|
||||||
|
|
||||||
|
|
||||||
|
def example_instantiation_logging():
|
||||||
|
"""
|
||||||
|
Example focused on logging and analyzing instantiation patterns.
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("INSTANTIATION LOGGING AND ANALYSIS EXAMPLE")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
class LoggingController(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):
|
||||||
|
"""
|
||||||
|
Log all instantiation attempts for later analysis.
|
||||||
|
"""
|
||||||
|
entry = {
|
||||||
|
'quantifier': str(quantifier),
|
||||||
|
'instantiation': str(instantiation),
|
||||||
|
'timestamp': len(self.log)
|
||||||
|
}
|
||||||
|
self.log.append(entry)
|
||||||
|
|
||||||
|
# For this example, allow all instantiations
|
||||||
|
return True
|
||||||
|
|
||||||
|
def push(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pop(self, num_scopes):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fresh(self, new_ctx):
|
||||||
|
return LoggingController(ctx=new_ctx)
|
||||||
|
|
||||||
|
def analyze_patterns(self):
|
||||||
|
"""Analyze logged instantiation patterns."""
|
||||||
|
print(f"Total instantiations logged: {len(self.log)}")
|
||||||
|
|
||||||
|
# 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'])
|
||||||
|
|
||||||
|
for q, instantiations in by_quantifier.items():
|
||||||
|
print(f"\nQuantifier: {q}")
|
||||||
|
print(f" Instantiations ({len(instantiations)}):")
|
||||||
|
for i, inst in enumerate(instantiations, 1):
|
||||||
|
print(f" {i}. {inst}")
|
||||||
|
|
||||||
|
# Create solver with logging controller
|
||||||
|
s = Solver()
|
||||||
|
controller = LoggingController(s)
|
||||||
|
|
||||||
|
# Create scenario with multiple quantifiers
|
||||||
|
x = Int('x')
|
||||||
|
f = Function('f', IntSort(), IntSort())
|
||||||
|
g = Function('g', IntSort(), IntSort())
|
||||||
|
|
||||||
|
# Add multiple quantified axioms
|
||||||
|
s.add(ForAll([x], f(x) >= x)) # f(x) is at least x
|
||||||
|
s.add(ForAll([x], g(x) <= x)) # g(x) is at most x
|
||||||
|
|
||||||
|
# Add constraints to trigger instantiations
|
||||||
|
a, b = Ints('a b')
|
||||||
|
s.add(f(a) < 5)
|
||||||
|
s.add(g(b) > 10)
|
||||||
|
s.add(a == 2)
|
||||||
|
s.add(b == 8)
|
||||||
|
|
||||||
|
print("Solving with full logging enabled...")
|
||||||
|
result = s.check()
|
||||||
|
print(f"Result: {result}")
|
||||||
|
|
||||||
|
# Analyze the logged instantiation patterns
|
||||||
|
print("\nInstantiation Analysis:")
|
||||||
|
controller.analyze_patterns()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run all examples
|
||||||
|
example_basic_quantifier_control()
|
||||||
|
example_advanced_filtering()
|
||||||
|
example_instantiation_logging()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("All examples completed successfully!")
|
||||||
|
print("=" * 60)
|
377
src/test/test_quantifier_instantiation_callback.py
Normal file
377
src/test/test_quantifier_instantiation_callback.py
Normal file
|
@ -0,0 +1,377 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Unit tests for the UserPropagator quantifier instantiation callback feature.
|
||||||
|
|
||||||
|
This test suite validates the functionality added in Z3 version 4.15.3 that allows
|
||||||
|
user propagators to intercept and control quantifier instantiations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add the Z3 Python bindings to the path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'build', 'python'))
|
||||||
|
|
||||||
|
from z3 import *
|
||||||
|
|
||||||
|
class TestQuantifierInstantiationCallback(unittest.TestCase):
|
||||||
|
"""Test cases for quantifier instantiation callback functionality."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.reset_counters()
|
||||||
|
|
||||||
|
def reset_counters(self):
|
||||||
|
"""Reset global test counters."""
|
||||||
|
global g_instantiation_count, g_allowed_count, g_blocked_count
|
||||||
|
g_instantiation_count = 0
|
||||||
|
g_allowed_count = 0
|
||||||
|
g_blocked_count = 0
|
||||||
|
|
||||||
|
# Global counters for test validation
|
||||||
|
g_instantiation_count = 0
|
||||||
|
g_allowed_count = 0
|
||||||
|
g_blocked_count = 0
|
||||||
|
|
||||||
|
class BasicInstantiationController(UserPropagateBase):
|
||||||
|
"""Basic user propagator for testing instantiation control."""
|
||||||
|
|
||||||
|
def __init__(self, allow_first_n=3, s=None, ctx=None):
|
||||||
|
UserPropagateBase.__init__(self, s, ctx)
|
||||||
|
self.allow_first_n = allow_first_n
|
||||||
|
self.add_on_binding(self.control_instantiation)
|
||||||
|
|
||||||
|
def control_instantiation(self, quantifier, instantiation):
|
||||||
|
global g_instantiation_count, g_allowed_count, g_blocked_count
|
||||||
|
g_instantiation_count += 1
|
||||||
|
|
||||||
|
allow = g_instantiation_count <= self.allow_first_n
|
||||||
|
if allow:
|
||||||
|
g_allowed_count += 1
|
||||||
|
else:
|
||||||
|
g_blocked_count += 1
|
||||||
|
|
||||||
|
return allow
|
||||||
|
|
||||||
|
def push(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pop(self, num_scopes):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fresh(self, new_ctx):
|
||||||
|
return BasicInstantiationController(self.allow_first_n, ctx=new_ctx)
|
||||||
|
|
||||||
|
class LoggingController(UserPropagateBase):
|
||||||
|
"""User propagator that logs all instantiations without blocking."""
|
||||||
|
|
||||||
|
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):
|
||||||
|
global g_instantiation_count
|
||||||
|
g_instantiation_count += 1
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
'quantifier': str(quantifier),
|
||||||
|
'instantiation': str(instantiation),
|
||||||
|
'count': g_instantiation_count
|
||||||
|
}
|
||||||
|
self.log.append(entry)
|
||||||
|
|
||||||
|
# Allow all instantiations
|
||||||
|
return True
|
||||||
|
|
||||||
|
def push(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pop(self, num_scopes):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fresh(self, new_ctx):
|
||||||
|
return LoggingController(ctx=new_ctx)
|
||||||
|
|
||||||
|
class PatternFilterController(UserPropagateBase):
|
||||||
|
"""User propagator that filters based on instantiation patterns."""
|
||||||
|
|
||||||
|
def __init__(self, max_per_pattern=2, s=None, ctx=None):
|
||||||
|
UserPropagateBase.__init__(self, s, ctx)
|
||||||
|
self.max_per_pattern = max_per_pattern
|
||||||
|
self.pattern_counts = {}
|
||||||
|
self.add_on_binding(self.filter_by_pattern)
|
||||||
|
|
||||||
|
def filter_by_pattern(self, quantifier, instantiation):
|
||||||
|
global g_instantiation_count, g_allowed_count, g_blocked_count
|
||||||
|
g_instantiation_count += 1
|
||||||
|
|
||||||
|
pattern = str(instantiation)
|
||||||
|
self.pattern_counts[pattern] = self.pattern_counts.get(pattern, 0) + 1
|
||||||
|
|
||||||
|
allow = self.pattern_counts[pattern] <= self.max_per_pattern
|
||||||
|
if allow:
|
||||||
|
g_allowed_count += 1
|
||||||
|
else:
|
||||||
|
g_blocked_count += 1
|
||||||
|
|
||||||
|
return allow
|
||||||
|
|
||||||
|
def push(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pop(self, num_scopes):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fresh(self, new_ctx):
|
||||||
|
return PatternFilterController(self.max_per_pattern, ctx=new_ctx)
|
||||||
|
|
||||||
|
class TestQuantifierInstantiationCallback(unittest.TestCase):
|
||||||
|
"""Test cases for quantifier instantiation callback functionality."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.reset_counters()
|
||||||
|
|
||||||
|
def reset_counters(self):
|
||||||
|
"""Reset global test counters."""
|
||||||
|
global g_instantiation_count, g_allowed_count, g_blocked_count
|
||||||
|
g_instantiation_count = 0
|
||||||
|
g_allowed_count = 0
|
||||||
|
g_blocked_count = 0
|
||||||
|
|
||||||
|
def test_basic_instantiation_control(self):
|
||||||
|
"""Test basic instantiation limiting functionality."""
|
||||||
|
self.reset_counters()
|
||||||
|
|
||||||
|
# Create solver with instantiation controller
|
||||||
|
s = Solver()
|
||||||
|
controller = BasicInstantiationController(allow_first_n=2, s=s)
|
||||||
|
|
||||||
|
# Create simple quantified formula
|
||||||
|
x = Int('x')
|
||||||
|
f = Function('f', IntSort(), IntSort())
|
||||||
|
|
||||||
|
# Add quantified axiom: forall x. f(x) >= 0
|
||||||
|
s.add(ForAll([x], f(x) >= 0))
|
||||||
|
|
||||||
|
# Add constraints that trigger instantiations
|
||||||
|
a, b, c = Ints('a b c')
|
||||||
|
s.add(f(a) < 10)
|
||||||
|
s.add(f(b) < 20)
|
||||||
|
s.add(f(c) < 30)
|
||||||
|
s.add(a == 1, b == 2, c == 3)
|
||||||
|
|
||||||
|
# Solve
|
||||||
|
result = s.check()
|
||||||
|
|
||||||
|
# Validate that callbacks were invoked and some instantiations were blocked
|
||||||
|
self.assertGreater(g_instantiation_count, 0, "No instantiations were attempted")
|
||||||
|
self.assertEqual(g_allowed_count, 2, "Expected exactly 2 allowed instantiations")
|
||||||
|
self.assertGreaterEqual(g_blocked_count, 0, "Expected some blocked instantiations")
|
||||||
|
|
||||||
|
# Should still be satisfiable with limited instantiations
|
||||||
|
self.assertEqual(result, sat, "Formula should be satisfiable")
|
||||||
|
|
||||||
|
def test_logging_all_instantiations(self):
|
||||||
|
"""Test that logging controller captures all instantiation attempts."""
|
||||||
|
self.reset_counters()
|
||||||
|
|
||||||
|
s = Solver()
|
||||||
|
logger = LoggingController(s=s)
|
||||||
|
|
||||||
|
# Create formula with multiple quantifiers
|
||||||
|
x = Int('x')
|
||||||
|
f = Function('f', IntSort(), IntSort())
|
||||||
|
g = Function('g', IntSort(), IntSort())
|
||||||
|
|
||||||
|
s.add(ForAll([x], f(x) >= x))
|
||||||
|
s.add(ForAll([x], g(x) <= x))
|
||||||
|
|
||||||
|
a, b = Ints('a b')
|
||||||
|
s.add(f(a) < 5)
|
||||||
|
s.add(g(b) > 10)
|
||||||
|
s.add(a == 2, b == 8)
|
||||||
|
|
||||||
|
result = s.check()
|
||||||
|
|
||||||
|
# Validate logging
|
||||||
|
self.assertGreater(len(logger.log), 0, "No instantiations were logged")
|
||||||
|
self.assertEqual(len(logger.log), g_instantiation_count, "Log count mismatch")
|
||||||
|
|
||||||
|
# Check log structure
|
||||||
|
for entry in logger.log:
|
||||||
|
self.assertIn('quantifier', entry)
|
||||||
|
self.assertIn('instantiation', entry)
|
||||||
|
self.assertIn('count', entry)
|
||||||
|
self.assertIsInstance(entry['quantifier'], str)
|
||||||
|
self.assertIsInstance(entry['instantiation'], str)
|
||||||
|
self.assertIsInstance(entry['count'], int)
|
||||||
|
|
||||||
|
def test_pattern_based_filtering(self):
|
||||||
|
"""Test pattern-based instantiation filtering."""
|
||||||
|
self.reset_counters()
|
||||||
|
|
||||||
|
s = Solver()
|
||||||
|
filter_controller = PatternFilterController(max_per_pattern=1, s=s)
|
||||||
|
|
||||||
|
# Create scenario that might generate duplicate patterns
|
||||||
|
x, y = Ints('x y')
|
||||||
|
P = Function('P', IntSort(), IntSort(), BoolSort())
|
||||||
|
|
||||||
|
s.add(ForAll([x, y], Implies(P(x, y), P(y, x))))
|
||||||
|
|
||||||
|
a, b, c = Ints('a b c')
|
||||||
|
s.add(P(a, b))
|
||||||
|
s.add(P(b, c))
|
||||||
|
s.add(P(a, c)) # This might create similar patterns
|
||||||
|
|
||||||
|
result = s.check()
|
||||||
|
|
||||||
|
# Validate that filtering occurred
|
||||||
|
self.assertGreater(g_instantiation_count, 0, "No instantiations were attempted")
|
||||||
|
|
||||||
|
# Check that patterns were tracked
|
||||||
|
self.assertGreater(len(filter_controller.pattern_counts), 0, "No patterns were tracked")
|
||||||
|
|
||||||
|
# Verify some patterns appeared multiple times (and were filtered)
|
||||||
|
max_count = max(filter_controller.pattern_counts.values())
|
||||||
|
if max_count > 1:
|
||||||
|
self.assertGreater(g_blocked_count, 0, "Expected some blocked instantiations for repeated patterns")
|
||||||
|
|
||||||
|
def test_callback_with_unsat_formula(self):
|
||||||
|
"""Test callback behavior with unsatisfiable formulas."""
|
||||||
|
self.reset_counters()
|
||||||
|
|
||||||
|
s = Solver()
|
||||||
|
controller = BasicInstantiationController(allow_first_n=1, s=s)
|
||||||
|
|
||||||
|
# Create unsatisfiable formula
|
||||||
|
x = Int('x')
|
||||||
|
f = Function('f', IntSort(), IntSort())
|
||||||
|
|
||||||
|
s.add(ForAll([x], f(x) >= 0))
|
||||||
|
s.add(f(5) < -10) # Contradiction
|
||||||
|
|
||||||
|
result = s.check()
|
||||||
|
|
||||||
|
# Should be UNSAT regardless of instantiation control
|
||||||
|
self.assertEqual(result, unsat, "Formula should be unsatisfiable")
|
||||||
|
|
||||||
|
# Callbacks may or may not be invoked for UNSAT formulas
|
||||||
|
# (depends on solver's instantiation strategy)
|
||||||
|
|
||||||
|
def test_callback_registration_without_instantiations(self):
|
||||||
|
"""Test that callback registration works even without quantifier instantiations."""
|
||||||
|
self.reset_counters()
|
||||||
|
|
||||||
|
s = Solver()
|
||||||
|
controller = BasicInstantiationController(allow_first_n=5, s=s)
|
||||||
|
|
||||||
|
# Add formula without quantifiers
|
||||||
|
x, y = Ints('x y')
|
||||||
|
s.add(x + y == 10)
|
||||||
|
s.add(x > 5)
|
||||||
|
|
||||||
|
result = s.check()
|
||||||
|
|
||||||
|
# Should solve without invoking callbacks
|
||||||
|
self.assertEqual(result, sat, "Formula should be satisfiable")
|
||||||
|
self.assertEqual(g_instantiation_count, 0, "No instantiations should occur without quantifiers")
|
||||||
|
|
||||||
|
def test_multiple_solvers_independent_callbacks(self):
|
||||||
|
"""Test that callbacks on different solvers are independent."""
|
||||||
|
self.reset_counters()
|
||||||
|
|
||||||
|
# Create first solver with restrictive controller
|
||||||
|
s1 = Solver()
|
||||||
|
controller1 = BasicInstantiationController(allow_first_n=1, s=s1)
|
||||||
|
|
||||||
|
# Create second solver with permissive controller
|
||||||
|
s2 = Solver()
|
||||||
|
controller2 = BasicInstantiationController(allow_first_n=10, s=s2)
|
||||||
|
|
||||||
|
# Add same formula to both
|
||||||
|
x = Int('x')
|
||||||
|
f = Function('f', IntSort(), IntSort())
|
||||||
|
axiom = ForAll([x], f(x) >= 0)
|
||||||
|
|
||||||
|
for s in [s1, s2]:
|
||||||
|
s.add(axiom)
|
||||||
|
a = Int(f'a_{id(s)}') # Unique constant per solver
|
||||||
|
s.add(f(a) < 5)
|
||||||
|
s.add(a == 1)
|
||||||
|
|
||||||
|
# Solve both
|
||||||
|
result1 = s1.check()
|
||||||
|
result2 = s2.check()
|
||||||
|
|
||||||
|
# Both should be satisfiable
|
||||||
|
self.assertEqual(result1, sat, "First solver should find solution")
|
||||||
|
self.assertEqual(result2, sat, "Second solver should find solution")
|
||||||
|
|
||||||
|
# Note: Since we use global counters, we can't easily test independence
|
||||||
|
# In a real implementation, each controller would have its own counters
|
||||||
|
|
||||||
|
class TestErrorConditions(unittest.TestCase):
|
||||||
|
"""Test error conditions and edge cases."""
|
||||||
|
|
||||||
|
def test_callback_exception_handling(self):
|
||||||
|
"""Test that exceptions in callbacks are handled gracefully."""
|
||||||
|
|
||||||
|
class FaultyController(UserPropagateBase):
|
||||||
|
def __init__(self, s=None, ctx=None):
|
||||||
|
UserPropagateBase.__init__(self, s, ctx)
|
||||||
|
self.add_on_binding(self.faulty_callback)
|
||||||
|
|
||||||
|
def faulty_callback(self, quantifier, instantiation):
|
||||||
|
# This callback always raises an exception
|
||||||
|
raise ValueError("Test exception in callback")
|
||||||
|
|
||||||
|
def push(self): pass
|
||||||
|
def pop(self, num_scopes): pass
|
||||||
|
def fresh(self, new_ctx): return FaultyController(ctx=new_ctx)
|
||||||
|
|
||||||
|
s = Solver()
|
||||||
|
|
||||||
|
# This should not crash, but the callback behavior is implementation-defined
|
||||||
|
# when exceptions occur
|
||||||
|
try:
|
||||||
|
controller = FaultyController(s=s)
|
||||||
|
|
||||||
|
x = Int('x')
|
||||||
|
f = Function('f', IntSort(), IntSort())
|
||||||
|
s.add(ForAll([x], f(x) >= 0))
|
||||||
|
s.add(f(1) < 5)
|
||||||
|
|
||||||
|
# The solver should handle callback exceptions gracefully
|
||||||
|
# Exact behavior may vary, but should not crash
|
||||||
|
result = s.check()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# If an exception propagates, it should be the one we raised
|
||||||
|
# or a Z3Exception wrapping it
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Check if Z3 is available
|
||||||
|
try:
|
||||||
|
from z3 import *
|
||||||
|
|
||||||
|
# Test basic Z3 functionality
|
||||||
|
x = Int('x')
|
||||||
|
s = Solver()
|
||||||
|
s.add(x > 0)
|
||||||
|
if s.check() != sat:
|
||||||
|
print("ERROR: Basic Z3 functionality not working")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: Z3 Python bindings not found")
|
||||||
|
print("Make sure PYTHONPATH includes the Z3 Python bindings directory")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Run the tests
|
||||||
|
print("Running quantifier instantiation callback tests...")
|
||||||
|
unittest.main(verbosity=2)
|
Loading…
Add table
Add a link
Reference in a new issue