smtconfig 1.0.2__tar.gz
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.
- smtconfig-1.0.2/LICENSE +11 -0
- smtconfig-1.0.2/PKG-INFO +31 -0
- smtconfig-1.0.2/README.md +13 -0
- smtconfig-1.0.2/pyproject.toml +30 -0
- smtconfig-1.0.2/setup.cfg +4 -0
- smtconfig-1.0.2/smtconfig/__init__.py +2 -0
- smtconfig-1.0.2/smtconfig/config.py +794 -0
- smtconfig-1.0.2/smtconfig/utils.py +61 -0
- smtconfig-1.0.2/smtconfig/vis.py +182 -0
- smtconfig-1.0.2/smtconfig.egg-info/PKG-INFO +31 -0
- smtconfig-1.0.2/smtconfig.egg-info/SOURCES.txt +16 -0
- smtconfig-1.0.2/smtconfig.egg-info/dependency_links.txt +1 -0
- smtconfig-1.0.2/smtconfig.egg-info/requires.txt +7 -0
- smtconfig-1.0.2/smtconfig.egg-info/top_level.txt +1 -0
smtconfig-1.0.2/LICENSE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Copyright 2016 Nikolai Krivoshchapov
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
|
|
7
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
8
|
+
|
|
9
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
10
|
+
|
|
11
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
smtconfig-1.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smtconfig
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Declarative SMT-based configuration system built on top of Z3
|
|
5
|
+
Author: Nikolai Krivoshchapov
|
|
6
|
+
License: BSD 3-Clause License
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: z3-solver
|
|
11
|
+
Requires-Dist: pydantic
|
|
12
|
+
Requires-Dist: networkx
|
|
13
|
+
Requires-Dist: pandas
|
|
14
|
+
Requires-Dist: rich
|
|
15
|
+
Requires-Dist: textual
|
|
16
|
+
Requires-Dist: icecream
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
## SMTConfig
|
|
20
|
+
|
|
21
|
+
Configuration validation and completion using z3 solver.
|
|
22
|
+
|
|
23
|
+
### Installation
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
pip install smtconfig
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Example
|
|
30
|
+
|
|
31
|
+
[see here](https://gitlab.com/knvvv/smtconfig/-/blob/master/example.py)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "smtconfig"
|
|
7
|
+
version = "1.0.2"
|
|
8
|
+
description = "Declarative SMT-based configuration system built on top of Z3"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "BSD 3-Clause License" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Nikolai Krivoshchapov" }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"z3-solver",
|
|
18
|
+
"pydantic",
|
|
19
|
+
"networkx",
|
|
20
|
+
"pandas",
|
|
21
|
+
"rich",
|
|
22
|
+
"textual",
|
|
23
|
+
"icecream"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
packages = ["smtconfig"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-dir]
|
|
30
|
+
"" = "."
|
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Literal, Optional, Iterable, Mapping, Type, Any, Self
|
|
3
|
+
import pydantic as pc
|
|
4
|
+
import networkx as nx
|
|
5
|
+
import itertools
|
|
6
|
+
import z3
|
|
7
|
+
import enum
|
|
8
|
+
from icecream import ic
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from .utils import free_vars, SimpleTypesZ3, OptTypesZ3, ArbTypesModel
|
|
11
|
+
|
|
12
|
+
DECLARED_SORTS = set()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SimpleZ3Type(ArbTypesModel):
|
|
16
|
+
py_type: Type
|
|
17
|
+
z3_sort: z3.SortRef
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_py(cls, input: Type) -> Self:
|
|
21
|
+
assert input in SimpleTypesZ3, f"'{input}' is not supported as simple type"
|
|
22
|
+
return cls(
|
|
23
|
+
py_type=input,
|
|
24
|
+
z3_sort=SimpleTypesZ3[input],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def get_constraint_value(self, input: Any):
|
|
28
|
+
if self.py_type is int:
|
|
29
|
+
return int(input)
|
|
30
|
+
elif self.py_type is float:
|
|
31
|
+
return float(input)
|
|
32
|
+
elif self.py_type is bool:
|
|
33
|
+
return bool(input)
|
|
34
|
+
elif self.py_type is str:
|
|
35
|
+
return z3.StringVal(input)
|
|
36
|
+
else:
|
|
37
|
+
raise Exception("Unknown type")
|
|
38
|
+
|
|
39
|
+
def from_z3(self, input: Any, m: z3.Model):
|
|
40
|
+
if self.py_type is int:
|
|
41
|
+
return int(str(input))
|
|
42
|
+
elif self.py_type is float:
|
|
43
|
+
return float(str(input))
|
|
44
|
+
elif self.py_type is bool:
|
|
45
|
+
return bool(z3.is_true(input))
|
|
46
|
+
elif self.py_type is str:
|
|
47
|
+
return input.as_string()
|
|
48
|
+
else:
|
|
49
|
+
raise Exception("Unknown type")
|
|
50
|
+
|
|
51
|
+
def pretty_str(self):
|
|
52
|
+
return str(self.z3_sort)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class EnumZ3Type(ArbTypesModel):
|
|
56
|
+
enum_type: Type | list[str]
|
|
57
|
+
z3_sort: z3.DatatypeSortRef
|
|
58
|
+
z2py: dict[z3.DatatypeRef, Any]
|
|
59
|
+
py2z: dict[Any, z3.DatatypeRef]
|
|
60
|
+
name: str
|
|
61
|
+
|
|
62
|
+
def get_constraint_value(self, input: Any):
|
|
63
|
+
assert input in self.py2z
|
|
64
|
+
return self.py2z[input]
|
|
65
|
+
|
|
66
|
+
def from_z3(self, input: Any, m: z3.Model):
|
|
67
|
+
assert input in self.z2py
|
|
68
|
+
return self.z2py[input]
|
|
69
|
+
|
|
70
|
+
def pretty_str(self):
|
|
71
|
+
return str(self.z3_sort)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class OptionalZ3Type(ArbTypesModel):
|
|
75
|
+
py_type: Type
|
|
76
|
+
z3_sort: z3.DatatypeSortRef
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_py(cls, input: Type) -> Self:
|
|
80
|
+
assert input in OptTypesZ3, f"Optional types are not supported for '{input}'"
|
|
81
|
+
return cls(
|
|
82
|
+
py_type=input,
|
|
83
|
+
z3_sort=OptTypesZ3[input],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def get_constraint_value(self, input: Any):
|
|
87
|
+
if input is None:
|
|
88
|
+
return self.z3_sort.Nil
|
|
89
|
+
elif self.py_type is int:
|
|
90
|
+
val = int(input)
|
|
91
|
+
elif self.py_type is float:
|
|
92
|
+
val = float(input)
|
|
93
|
+
elif self.py_type is bool:
|
|
94
|
+
val = bool(input)
|
|
95
|
+
elif self.py_type is str:
|
|
96
|
+
val = z3.StringVal(input)
|
|
97
|
+
else:
|
|
98
|
+
raise Exception("Unknown type")
|
|
99
|
+
return self.z3_sort.Some(val)
|
|
100
|
+
|
|
101
|
+
def from_z3(self, input: Any, m: z3.Model):
|
|
102
|
+
if not m.eval(self.z3_sort.is_Some(input)):
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
val = m.eval(self.z3_sort.val(input))
|
|
106
|
+
# ic(input, val, val.sort(), val.sort().name())
|
|
107
|
+
if self.py_type is int:
|
|
108
|
+
res = val.as_long()
|
|
109
|
+
elif self.py_type is float:
|
|
110
|
+
res = float(val)
|
|
111
|
+
elif self.py_type is bool:
|
|
112
|
+
res = bool(val)
|
|
113
|
+
elif self.py_type is str:
|
|
114
|
+
res = val.as_string()
|
|
115
|
+
else:
|
|
116
|
+
raise Exception("Unknown type")
|
|
117
|
+
return res
|
|
118
|
+
|
|
119
|
+
def pretty_str(self):
|
|
120
|
+
return str(self.z3_sort)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Field(ArbTypesModel):
|
|
124
|
+
name: str
|
|
125
|
+
typ: SimpleZ3Type | EnumZ3Type | OptionalZ3Type
|
|
126
|
+
type_kind: Literal['simple', 'enum', 'optional']
|
|
127
|
+
z3var: z3.SeqRef | z3.BoolRef | z3.ArithRef | z3.DatatypeRef
|
|
128
|
+
default: Any
|
|
129
|
+
optional: bool
|
|
130
|
+
doc: Optional[str]
|
|
131
|
+
tracking_literal: z3.BoolRef
|
|
132
|
+
kind: Literal['field'] = 'field'
|
|
133
|
+
expr: Optional[z3.BoolRef] = None # Bad design, but should work for now...
|
|
134
|
+
|
|
135
|
+
def value_constraint(self, input: Any) -> z3.BoolRef:
|
|
136
|
+
value = self.typ.get_constraint_value(input)
|
|
137
|
+
return self.z3var == value
|
|
138
|
+
|
|
139
|
+
def from_z3(self, input: Any, m: z3.Model):
|
|
140
|
+
return self.typ.from_z3(input, m)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Rule(ArbTypesModel):
|
|
144
|
+
expr: z3.BoolRef
|
|
145
|
+
name: str
|
|
146
|
+
tags: tuple[str, ...]
|
|
147
|
+
tracking_literal: z3.BoolRef
|
|
148
|
+
kind: Literal['rule'] = 'rule'
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Config:
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
name: Optional[str] = None,
|
|
156
|
+
solver_options: Optional[dict] = None,
|
|
157
|
+
):
|
|
158
|
+
self._fields: dict[str, Field] = {}
|
|
159
|
+
self._rules: dict[str, Rule] = {}
|
|
160
|
+
self._enum_sorts: dict[str, EnumZ3Type] = {}
|
|
161
|
+
self._rule_count = 0
|
|
162
|
+
self._cast: dict = {}
|
|
163
|
+
|
|
164
|
+
def is_nil(self, field_name: str) -> z3.BoolRef:
|
|
165
|
+
assert type(field_name) is str, (
|
|
166
|
+
"Make sure you are passing field name, not Z3 constant")
|
|
167
|
+
field = self._fields[field_name]
|
|
168
|
+
assert field.optional, (
|
|
169
|
+
f"Attempted check for nil state for non-optional field '{field_name}'"
|
|
170
|
+
)
|
|
171
|
+
return field.z3var == field.typ.z3_sort.Nil
|
|
172
|
+
|
|
173
|
+
def not_nil(self, field_name: str) -> z3.BoolRef:
|
|
174
|
+
assert type(field_name) is str, (
|
|
175
|
+
"Make sure you are passing field name, not Z3 constant")
|
|
176
|
+
field = self._fields[field_name]
|
|
177
|
+
assert field.optional, (
|
|
178
|
+
f"Attempted check for nil state for non-optional field '{field_name}'"
|
|
179
|
+
)
|
|
180
|
+
return field.z3var != field.typ.z3_sort.Nil
|
|
181
|
+
|
|
182
|
+
def get_some(self, field_name: str, strict: bool = False):
|
|
183
|
+
assert type(field_name) is str, (
|
|
184
|
+
"Make sure you are passing field name, not Z3 constant")
|
|
185
|
+
field = self._fields[field_name]
|
|
186
|
+
if strict and not field.optional:
|
|
187
|
+
assert field.optional, (
|
|
188
|
+
f"Attempted check for nil state for non-optional field '{field_name}'"
|
|
189
|
+
)
|
|
190
|
+
elif not field.optional:
|
|
191
|
+
return field.z3var
|
|
192
|
+
else:
|
|
193
|
+
return field.typ.z3_sort.val(field.z3var)
|
|
194
|
+
|
|
195
|
+
def equivalent(self, *clauses):
|
|
196
|
+
if len(clauses) < 2:
|
|
197
|
+
return []
|
|
198
|
+
return (a == b for i, a in enumerate(clauses)
|
|
199
|
+
for j, b in enumerate(clauses) if i < j)
|
|
200
|
+
|
|
201
|
+
def distinct(self, *clauses):
|
|
202
|
+
if len(clauses) < 2:
|
|
203
|
+
return []
|
|
204
|
+
return (a != b for i, a in enumerate(clauses)
|
|
205
|
+
for j, b in enumerate(clauses) if i < j)
|
|
206
|
+
|
|
207
|
+
def exclusive(self, *clauses):
|
|
208
|
+
if len(clauses) < 2:
|
|
209
|
+
return tuple()
|
|
210
|
+
return (z3.Implies(true_clause, z3.Not(false_clause))
|
|
211
|
+
for i, true_clause in enumerate(clauses)
|
|
212
|
+
for j, false_clause in enumerate(clauses) if i != j)
|
|
213
|
+
|
|
214
|
+
def _mk_field_type(
|
|
215
|
+
self,
|
|
216
|
+
typ: Type,
|
|
217
|
+
optional: bool,
|
|
218
|
+
) -> SimpleZ3Type | OptionalZ3Type:
|
|
219
|
+
if optional:
|
|
220
|
+
return OptionalZ3Type.from_py(typ)
|
|
221
|
+
else:
|
|
222
|
+
return SimpleZ3Type.from_py(typ)
|
|
223
|
+
|
|
224
|
+
def _mk_z3_var(self, name: str, sort: z3.SortRef):
|
|
225
|
+
return z3.Const(name, sort)
|
|
226
|
+
|
|
227
|
+
def add_field(
|
|
228
|
+
self,
|
|
229
|
+
name: str,
|
|
230
|
+
typ: Type | list[str],
|
|
231
|
+
*,
|
|
232
|
+
default: Any = None,
|
|
233
|
+
optional: bool = False,
|
|
234
|
+
doc: Optional[str] = None,
|
|
235
|
+
) -> Field:
|
|
236
|
+
if name in self._fields:
|
|
237
|
+
raise ValueError(f"Field '{name}' already exists")
|
|
238
|
+
|
|
239
|
+
if isinstance(typ, list) or issubclass(typ, enum.Enum):
|
|
240
|
+
return self._add_enum(
|
|
241
|
+
name=name,
|
|
242
|
+
enum_cls=typ,
|
|
243
|
+
default=default,
|
|
244
|
+
optional=optional,
|
|
245
|
+
doc=doc,
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
return self._add_basic_field(
|
|
249
|
+
name=name,
|
|
250
|
+
typ=typ,
|
|
251
|
+
default=default,
|
|
252
|
+
optional=optional,
|
|
253
|
+
doc=doc,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def _add_basic_field(
|
|
257
|
+
self,
|
|
258
|
+
name: str,
|
|
259
|
+
typ: Type,
|
|
260
|
+
*,
|
|
261
|
+
default: Any = None,
|
|
262
|
+
optional: bool = False,
|
|
263
|
+
doc: Optional[str] = None,
|
|
264
|
+
) -> Field:
|
|
265
|
+
type_obj = self._mk_field_type(typ, optional)
|
|
266
|
+
# ic(name, type_obj)
|
|
267
|
+
z3var = self._mk_z3_var(name, type_obj.z3_sort)
|
|
268
|
+
tracking_literal = z3.Bool(f"__var_track_{name}")
|
|
269
|
+
field = Field(name=name,
|
|
270
|
+
typ=type_obj,
|
|
271
|
+
type_kind='simple',
|
|
272
|
+
z3var=z3var,
|
|
273
|
+
default=default,
|
|
274
|
+
optional=optional,
|
|
275
|
+
doc=doc,
|
|
276
|
+
tracking_literal=tracking_literal)
|
|
277
|
+
self._fields[name] = field
|
|
278
|
+
return field
|
|
279
|
+
|
|
280
|
+
def _mk_enum_sort(
|
|
281
|
+
self,
|
|
282
|
+
enum_cls: Type | list[str],
|
|
283
|
+
sort_name: Optional[str] = None,
|
|
284
|
+
) -> EnumZ3Type:
|
|
285
|
+
if isinstance(enum_cls, type) and issubclass(enum_cls, enum.Enum):
|
|
286
|
+
mode = 'enum'
|
|
287
|
+
elif isinstance(enum_cls, list) and all(
|
|
288
|
+
type(x) is str for x in enum_cls):
|
|
289
|
+
mode = 'list'
|
|
290
|
+
else:
|
|
291
|
+
raise TypeError(
|
|
292
|
+
f"Enum types created from either a subclass of enum.Enum or a list of string values. Got {enum_cls}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
enum_names: list[str]
|
|
296
|
+
if mode == 'enum':
|
|
297
|
+
enum_names = [m.name for m in enum_cls]
|
|
298
|
+
else:
|
|
299
|
+
enum_names = list(enum_cls)
|
|
300
|
+
assert len(enum_names) == len(set(enum_names)), (
|
|
301
|
+
f"Duplicates are found in the list of options: {enum_names}")
|
|
302
|
+
|
|
303
|
+
signature = self._get_enum_signature(enum_cls, enum_names, mode)
|
|
304
|
+
if signature in self._enum_sorts:
|
|
305
|
+
return self._enum_sorts[signature]
|
|
306
|
+
|
|
307
|
+
if sort_name is None and mode == 'enum':
|
|
308
|
+
sort_name = enum_cls.__name__
|
|
309
|
+
elif sort_name is None and mode == 'list':
|
|
310
|
+
i = 0
|
|
311
|
+
while True:
|
|
312
|
+
i += 1
|
|
313
|
+
sort_name = f"Enum{i}"
|
|
314
|
+
if all(sort.name != sort_name for sort in self._enum_sorts.
|
|
315
|
+
values()) and sort_name not in DECLARED_SORTS:
|
|
316
|
+
break
|
|
317
|
+
|
|
318
|
+
# ic(sort_name)
|
|
319
|
+
DECLARED_SORTS.add(sort_name)
|
|
320
|
+
sort, z3_vals = z3.EnumSort(f"{sort_name}_EnumSort", enum_names)
|
|
321
|
+
|
|
322
|
+
# Complicated updates for convenient casting
|
|
323
|
+
py2z = {m: z3_vals[i] for i, m in enumerate(enum_cls)}
|
|
324
|
+
z2py = {v: k for k, v in py2z.items()}
|
|
325
|
+
self._cast.update(py2z)
|
|
326
|
+
self._cast.update(z2py)
|
|
327
|
+
if mode == 'enum':
|
|
328
|
+
py2z.update({k.name: v for k, v in py2z.items()})
|
|
329
|
+
self._cast.update({
|
|
330
|
+
(enum_cls.__name__, k.name): v
|
|
331
|
+
for k, v in py2z.items()
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
enum_type = EnumZ3Type(
|
|
335
|
+
enum_type=enum_cls,
|
|
336
|
+
z3_sort=sort,
|
|
337
|
+
z2py=z2py,
|
|
338
|
+
py2z=py2z,
|
|
339
|
+
name=sort_name,
|
|
340
|
+
)
|
|
341
|
+
self._enum_sorts[signature] = enum_type
|
|
342
|
+
return enum_type
|
|
343
|
+
|
|
344
|
+
def _get_enum_signature(
|
|
345
|
+
self,
|
|
346
|
+
enum_cls: Type | list[str],
|
|
347
|
+
enum_names: list[str],
|
|
348
|
+
mode: Literal['enum', 'list'],
|
|
349
|
+
) -> str:
|
|
350
|
+
if mode == 'enum':
|
|
351
|
+
sig = enum_cls.__name__
|
|
352
|
+
if sig in self._enum_sorts:
|
|
353
|
+
orig_sort = self._enum_sorts[sig]
|
|
354
|
+
assert sorted([x.name
|
|
355
|
+
for x in orig_sort.enum_type]) == enum_names
|
|
356
|
+
return sig
|
|
357
|
+
else:
|
|
358
|
+
return tuple(sorted(enum_names))
|
|
359
|
+
|
|
360
|
+
def _add_enum(
|
|
361
|
+
self,
|
|
362
|
+
name: str,
|
|
363
|
+
enum_cls: Type | list[str],
|
|
364
|
+
*,
|
|
365
|
+
default: Any = None,
|
|
366
|
+
sort_name: Optional[str] = None,
|
|
367
|
+
optional: bool = False,
|
|
368
|
+
doc: Optional[str] = None,
|
|
369
|
+
) -> Field:
|
|
370
|
+
assert not optional, "Optional mode is not supported for enums. Include null case into your enum."
|
|
371
|
+
assert default is None or default in enum_cls, (
|
|
372
|
+
f"default value '{default}' must be a member of {enum_cls.__name__}"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
ctx = z3.main_ctx()
|
|
376
|
+
enum_sort = self._mk_enum_sort(enum_cls, sort_name)
|
|
377
|
+
z3var = z3.Const(name, enum_sort.z3_sort)
|
|
378
|
+
tracking_literal = z3.Bool(f"__var_track_{name}")
|
|
379
|
+
|
|
380
|
+
field = Field(
|
|
381
|
+
name=name,
|
|
382
|
+
typ=enum_sort,
|
|
383
|
+
type_kind='enum',
|
|
384
|
+
z3var=z3var,
|
|
385
|
+
default=enum_sort.py2z[default] if default is not None else None,
|
|
386
|
+
optional=optional,
|
|
387
|
+
doc=doc,
|
|
388
|
+
tracking_literal=tracking_literal,
|
|
389
|
+
)
|
|
390
|
+
self._fields[name] = field
|
|
391
|
+
return field
|
|
392
|
+
|
|
393
|
+
def __getitem__(self, key: str) -> z3.ExprRef:
|
|
394
|
+
return self._fields[key].z3var
|
|
395
|
+
|
|
396
|
+
def cast(self, x):
|
|
397
|
+
if x in self._cast:
|
|
398
|
+
return self._cast[x]
|
|
399
|
+
else:
|
|
400
|
+
return x
|
|
401
|
+
|
|
402
|
+
def add_rules(
|
|
403
|
+
self,
|
|
404
|
+
exprs: Iterable[z3.BoolRef],
|
|
405
|
+
name: Optional[str] = None,
|
|
406
|
+
tags: Iterable[str] = (),
|
|
407
|
+
) -> list[Rule]:
|
|
408
|
+
return [
|
|
409
|
+
self.add_rule(expr,
|
|
410
|
+
name=f"{name}-{i}" if name is not None else None)
|
|
411
|
+
for i, expr in enumerate(exprs)
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
def add_rule(
|
|
415
|
+
self,
|
|
416
|
+
expr: z3.BoolRef,
|
|
417
|
+
*,
|
|
418
|
+
name: Optional[str] = None,
|
|
419
|
+
tags: Iterable[str] = (),
|
|
420
|
+
) -> Rule:
|
|
421
|
+
assert z3.is_bool(expr), (
|
|
422
|
+
f"expr must be a boolean z3 expression; got sort {getattr(expr, 'sort', lambda: None)()}"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
unique_name = name or f"rule_{self._rule_count}"
|
|
426
|
+
assert unique_name not in self._rules, f"Rule '{unique_name}' already exists"
|
|
427
|
+
self._rule_count += 1
|
|
428
|
+
|
|
429
|
+
tracking_literal = z3.Bool(f"__rule_track_{unique_name}")
|
|
430
|
+
tags_tuple = tuple(map(str, tags)) if tags is not None else tuple()
|
|
431
|
+
rule = Rule(
|
|
432
|
+
expr=expr,
|
|
433
|
+
name=unique_name,
|
|
434
|
+
tags=tags_tuple,
|
|
435
|
+
tracking_literal=tracking_literal,
|
|
436
|
+
)
|
|
437
|
+
self._rules[unique_name] = rule
|
|
438
|
+
return rule
|
|
439
|
+
|
|
440
|
+
def rules_on_fields(self, field_names: list[str]) -> list[Rule]:
|
|
441
|
+
field_names_set = {self._fields[name].z3var for name in field_names}
|
|
442
|
+
res = [
|
|
443
|
+
rule for rule in self._rules.values()
|
|
444
|
+
if len(free_vars(rule.expr).intersection(field_names_set)) > 0
|
|
445
|
+
]
|
|
446
|
+
return res
|
|
447
|
+
|
|
448
|
+
def new(
|
|
449
|
+
self,
|
|
450
|
+
mapping: Mapping[str, Any],
|
|
451
|
+
*,
|
|
452
|
+
partial: bool = True,
|
|
453
|
+
) -> "ConfigInstance":
|
|
454
|
+
# TODO: Find redundant input params
|
|
455
|
+
res = self._basic_new(
|
|
456
|
+
mapping=mapping,
|
|
457
|
+
partial=partial,
|
|
458
|
+
get_core=True,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
if res.status.sat() and partial:
|
|
462
|
+
uncertain_fields = {}
|
|
463
|
+
for name, field in self._fields.items():
|
|
464
|
+
if name in mapping or field.default is not None:
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
elim_res = self._basic_new(
|
|
468
|
+
mapping=mapping,
|
|
469
|
+
partial=partial,
|
|
470
|
+
extra_constraints=[
|
|
471
|
+
(z3.Not(field.value_constraint(res.solved[name])),
|
|
472
|
+
field.tracking_literal)
|
|
473
|
+
],
|
|
474
|
+
get_core=False)
|
|
475
|
+
if elim_res.status.sat():
|
|
476
|
+
uncertain_fields[name] = elim_res
|
|
477
|
+
res.uncertain_fields = uncertain_fields
|
|
478
|
+
elif res.status.unsat():
|
|
479
|
+
assert res.conflicts is not None
|
|
480
|
+
do_next_step = True
|
|
481
|
+
while do_next_step:
|
|
482
|
+
tmp_res = self._basic_new(
|
|
483
|
+
mapping=mapping,
|
|
484
|
+
partial=partial,
|
|
485
|
+
drop_rules=[
|
|
486
|
+
rule_name for core in res.conflicts
|
|
487
|
+
for rule_name in core.rules.keys()
|
|
488
|
+
],
|
|
489
|
+
get_core=True,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
do_next_step = tmp_res.status.unsat()
|
|
493
|
+
if do_next_step:
|
|
494
|
+
assert tmp_res.conflicts is not None
|
|
495
|
+
res.conflicts += tmp_res.conflicts
|
|
496
|
+
return res
|
|
497
|
+
|
|
498
|
+
def _basic_new(
|
|
499
|
+
self,
|
|
500
|
+
mapping: Mapping[str, Any],
|
|
501
|
+
*,
|
|
502
|
+
extra_constraints: list[tuple[z3.ExprRef, z3.BoolRef]] = [],
|
|
503
|
+
drop_rules: list[str] = [],
|
|
504
|
+
partial: bool = True,
|
|
505
|
+
get_core: bool = True,
|
|
506
|
+
) -> "ConfigInstance":
|
|
507
|
+
solver = z3.Solver()
|
|
508
|
+
provided = dict(mapping)
|
|
509
|
+
|
|
510
|
+
def add(expr, track):
|
|
511
|
+
if get_core:
|
|
512
|
+
solver.assert_and_track(expr, track)
|
|
513
|
+
else:
|
|
514
|
+
solver.assert_exprs(expr)
|
|
515
|
+
|
|
516
|
+
for name, field in self._fields.items():
|
|
517
|
+
if name in provided:
|
|
518
|
+
val = provided[name]
|
|
519
|
+
elif field.default is not None:
|
|
520
|
+
val = field.default
|
|
521
|
+
elif not partial and not field.optional:
|
|
522
|
+
raise Exception(f"Missing required field '{name}'")
|
|
523
|
+
else:
|
|
524
|
+
field.expr = None
|
|
525
|
+
continue
|
|
526
|
+
expr = field.value_constraint(val)
|
|
527
|
+
add(expr, field.tracking_literal)
|
|
528
|
+
field.expr = expr
|
|
529
|
+
|
|
530
|
+
for rule in self._rules.values():
|
|
531
|
+
if rule.name in drop_rules:
|
|
532
|
+
continue
|
|
533
|
+
add(rule.expr, rule.tracking_literal)
|
|
534
|
+
|
|
535
|
+
for constraint, lit in extra_constraints:
|
|
536
|
+
add(constraint, lit)
|
|
537
|
+
|
|
538
|
+
if get_core:
|
|
539
|
+
assert len(extra_constraints) == 0, (
|
|
540
|
+
"Cannot get unsat core with extra constraints yet")
|
|
541
|
+
tracking = {
|
|
542
|
+
x.tracking_literal: x
|
|
543
|
+
for x in itertools.chain(self._rules.values(),
|
|
544
|
+
self._fields.values())
|
|
545
|
+
# if x.name not in drop_rules
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
result = solver.check()
|
|
549
|
+
if result == z3.sat:
|
|
550
|
+
model = solver.model()
|
|
551
|
+
solved = {}
|
|
552
|
+
for name, field in self._fields.items():
|
|
553
|
+
new_val = model.eval(field.z3var, model_completion=True)
|
|
554
|
+
# ic(name, field, new_val)
|
|
555
|
+
solved[name] = field.from_z3(new_val, model)
|
|
556
|
+
|
|
557
|
+
return ConfigInstance(
|
|
558
|
+
config=self,
|
|
559
|
+
provided=provided,
|
|
560
|
+
solved=solved,
|
|
561
|
+
status=SMTResult.SAT,
|
|
562
|
+
model=model,
|
|
563
|
+
)
|
|
564
|
+
elif result == z3.unsat:
|
|
565
|
+
# TODO: Conflict resolution strats:
|
|
566
|
+
# constraint removal, max-smt, interactive
|
|
567
|
+
if get_core:
|
|
568
|
+
unsat_core = [tracking[c] for c in solver.unsat_core()]
|
|
569
|
+
disjoint_cores = self._get_disjoint_cores(unsat_core)
|
|
570
|
+
# NOTE: It seems that unsat_core always returns only one conn comp
|
|
571
|
+
assert len(disjoint_cores) == 1, (
|
|
572
|
+
"Expected one conn comp from unsat core")
|
|
573
|
+
conflicts = [
|
|
574
|
+
ConflictingConstraints(
|
|
575
|
+
values={c.name: c
|
|
576
|
+
for c in core if c.kind == 'field'},
|
|
577
|
+
rules={c.name: c
|
|
578
|
+
for c in core if c.kind == 'rule'},
|
|
579
|
+
) for core in disjoint_cores
|
|
580
|
+
]
|
|
581
|
+
else:
|
|
582
|
+
unsat_core = {}
|
|
583
|
+
conflicts = None
|
|
584
|
+
|
|
585
|
+
return ConfigInstance(config=self,
|
|
586
|
+
provided=provided,
|
|
587
|
+
status=SMTResult.UNSAT,
|
|
588
|
+
conflicts=conflicts)
|
|
589
|
+
else:
|
|
590
|
+
return ConfigInstance(config=self,
|
|
591
|
+
provided=provided,
|
|
592
|
+
status=SMTResult.UNKNOWN)
|
|
593
|
+
|
|
594
|
+
def _get_disjoint_cores(
|
|
595
|
+
self, unsat_core: list[Rule | Field]) -> list[list[Rule | Field]]:
|
|
596
|
+
gr = nx.Graph()
|
|
597
|
+
|
|
598
|
+
class NodeKind(enum.Enum):
|
|
599
|
+
EXPR = enum.auto()
|
|
600
|
+
SETVAR = enum.auto()
|
|
601
|
+
FREEVAR = enum.auto()
|
|
602
|
+
|
|
603
|
+
vartypes = {}
|
|
604
|
+
for x in unsat_core:
|
|
605
|
+
if x.kind == 'rule':
|
|
606
|
+
continue
|
|
607
|
+
vars = free_vars(x.expr)
|
|
608
|
+
assert len(vars) == 1
|
|
609
|
+
cur_var = next(iter(vars))
|
|
610
|
+
vartypes[x.name] = cur_var
|
|
611
|
+
|
|
612
|
+
for x in unsat_core:
|
|
613
|
+
if x.kind == 'field':
|
|
614
|
+
continue
|
|
615
|
+
ic(x.expr, free_vars(x.expr))
|
|
616
|
+
vars = free_vars(x.expr)
|
|
617
|
+
gr.add_node(x.name)
|
|
618
|
+
gr.nodes[x.name]['kind'] = NodeKind.EXPR
|
|
619
|
+
gr.add_nodes_from([(str(v), {
|
|
620
|
+
'kind':
|
|
621
|
+
NodeKind.SETVAR if str(v) in vartypes else NodeKind.FREEVAR
|
|
622
|
+
}) for v in vars])
|
|
623
|
+
gr.add_edges_from([(x.name, str(i)) for i in vars])
|
|
624
|
+
|
|
625
|
+
# ic(list(gr.nodes(data=True)))
|
|
626
|
+
# ic(list(gr.edges))
|
|
627
|
+
|
|
628
|
+
subg = gr.subgraph([
|
|
629
|
+
n for n, data in gr.nodes(data=True)
|
|
630
|
+
if data['kind'] != NodeKind.SETVAR
|
|
631
|
+
])
|
|
632
|
+
|
|
633
|
+
# ic(list(subg.nodes(data=True)))
|
|
634
|
+
# ic(list(subg.edges))
|
|
635
|
+
|
|
636
|
+
ind_cores = []
|
|
637
|
+
for comp in nx.connected_components(subg):
|
|
638
|
+
ind_gr_nodes = set(comp)
|
|
639
|
+
for rule_node in comp:
|
|
640
|
+
if gr.nodes[rule_node]['kind'] != NodeKind.EXPR:
|
|
641
|
+
continue
|
|
642
|
+
for nb in gr.neighbors(rule_node):
|
|
643
|
+
if gr.nodes[nb]['kind'] == NodeKind.SETVAR:
|
|
644
|
+
ind_gr_nodes.add(nb)
|
|
645
|
+
# ind_gr = gr.subgraph(ind_gr_nodes)
|
|
646
|
+
# ic(list(ind_gr.nodes(data=True)))
|
|
647
|
+
# ic(list(ind_gr.edges))
|
|
648
|
+
ind_cores.append([x for x in unsat_core if x.name in ind_gr_nodes])
|
|
649
|
+
|
|
650
|
+
return ind_cores
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
class ConflictingConstraints(ArbTypesModel):
|
|
654
|
+
values: dict[str, Field]
|
|
655
|
+
rules: dict[str, Rule]
|
|
656
|
+
|
|
657
|
+
def __repr__(self):
|
|
658
|
+
values_str = "\n".join([
|
|
659
|
+
f"{i}) {field.expr}"
|
|
660
|
+
for i, (name, field) in enumerate(self.values.items(), 1)
|
|
661
|
+
])
|
|
662
|
+
rule_str = "\n".join([
|
|
663
|
+
f"{i}) {rule.expr}"
|
|
664
|
+
for i, (name, rule) in enumerate(self.rules.items(), 1)
|
|
665
|
+
])
|
|
666
|
+
return f"""\
|
|
667
|
+
Field values:
|
|
668
|
+
{values_str}
|
|
669
|
+
are in conflict with rules:
|
|
670
|
+
{rule_str}
|
|
671
|
+
"""
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class SMTResult(enum.Enum):
|
|
675
|
+
SAT = enum.auto()
|
|
676
|
+
UNSAT = enum.auto()
|
|
677
|
+
UNKNOWN = enum.auto()
|
|
678
|
+
|
|
679
|
+
def __str__(self):
|
|
680
|
+
return str(self._name_)
|
|
681
|
+
|
|
682
|
+
def __repr__(self):
|
|
683
|
+
return str(self._name_)
|
|
684
|
+
|
|
685
|
+
def sat(self):
|
|
686
|
+
return self == SMTResult.SAT
|
|
687
|
+
|
|
688
|
+
def unsat(self):
|
|
689
|
+
return self == SMTResult.UNSAT
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
class FieldStatus(enum.Enum):
|
|
693
|
+
PROVIDED = enum.auto()
|
|
694
|
+
SOLVED = enum.auto()
|
|
695
|
+
CONFLICT = enum.auto()
|
|
696
|
+
UNCERTAIN = enum.auto()
|
|
697
|
+
UNSOLVED = enum.auto()
|
|
698
|
+
|
|
699
|
+
def __str__(self):
|
|
700
|
+
return str(self._name_).lower()
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
class FieldInst(ArbTypesModel):
|
|
704
|
+
status: FieldStatus
|
|
705
|
+
conflict_ids: Optional[list[int]] = None
|
|
706
|
+
|
|
707
|
+
@classmethod
|
|
708
|
+
def new(cls, status: FieldStatus, ids: Optional[list[int]] = None) -> Self:
|
|
709
|
+
return cls(
|
|
710
|
+
status=status,
|
|
711
|
+
conflict_ids=ids,
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
@pc.model_validator(mode='after')
|
|
715
|
+
def validate_conflict_ids(self) -> Self:
|
|
716
|
+
if self.status != FieldStatus.CONFLICT and self.conflict_ids is not None:
|
|
717
|
+
raise pc.ValidationError(
|
|
718
|
+
f"conflict_ids must be None when status is {self.status}, "
|
|
719
|
+
f"but got {self.conflict_ids}")
|
|
720
|
+
|
|
721
|
+
if self.status == FieldStatus.CONFLICT and (
|
|
722
|
+
self.conflict_ids is None or len(self.conflict_ids) == 0):
|
|
723
|
+
raise pc.ValidationError(
|
|
724
|
+
"conflict_ids must be provided when status is CONFLICT")
|
|
725
|
+
|
|
726
|
+
return self
|
|
727
|
+
|
|
728
|
+
def __str__(self):
|
|
729
|
+
if self.status == FieldStatus.CONFLICT:
|
|
730
|
+
assert self.conflict_ids is not None
|
|
731
|
+
return f"{self.status} ({','.join(str(x) for x in self.conflict_ids)})"
|
|
732
|
+
else:
|
|
733
|
+
return str(self.status)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
class ConfigInstance(ArbTypesModel):
|
|
737
|
+
config: Config
|
|
738
|
+
provided: dict
|
|
739
|
+
status: SMTResult
|
|
740
|
+
solved: Optional[dict] = pc.Field(default=None)
|
|
741
|
+
model: Optional[z3.ModelRef] = pc.Field(default=None)
|
|
742
|
+
conflicts: Optional[list[ConflictingConstraints]] = pc.Field(default=None)
|
|
743
|
+
uncertain_fields: Optional[dict[str,
|
|
744
|
+
ConfigInstance]] = pc.Field(default=None)
|
|
745
|
+
|
|
746
|
+
def build_table_dataframe(self) -> pd.DataFrame:
|
|
747
|
+
rows = []
|
|
748
|
+
|
|
749
|
+
handled_fields = set()
|
|
750
|
+
|
|
751
|
+
def handle_field(
|
|
752
|
+
key: str,
|
|
753
|
+
value: str | int | bool | None,
|
|
754
|
+
status: FieldInst,
|
|
755
|
+
) -> None:
|
|
756
|
+
if key not in self.config._fields:
|
|
757
|
+
return
|
|
758
|
+
type_str = self.config._fields[key].typ.pretty_str()
|
|
759
|
+
rows.append({
|
|
760
|
+
"Field": key,
|
|
761
|
+
"Value": str(value),
|
|
762
|
+
"Type": type_str,
|
|
763
|
+
"Status": status,
|
|
764
|
+
})
|
|
765
|
+
handled_fields.add(key)
|
|
766
|
+
|
|
767
|
+
if self.conflicts is not None:
|
|
768
|
+
for i, conflict in enumerate(self.conflicts, 1):
|
|
769
|
+
for key, field in conflict.values.items():
|
|
770
|
+
handle_field(key, self.provided[key],
|
|
771
|
+
FieldInst.new(FieldStatus.CONFLICT, [i]))
|
|
772
|
+
|
|
773
|
+
for key, v in self.provided.items():
|
|
774
|
+
if key in handled_fields:
|
|
775
|
+
continue
|
|
776
|
+
handle_field(key, v, FieldInst.new(FieldStatus.PROVIDED))
|
|
777
|
+
|
|
778
|
+
if self.uncertain_fields is not None:
|
|
779
|
+
assert self.solved is not None
|
|
780
|
+
for key in self.uncertain_fields:
|
|
781
|
+
handle_field(key, self.solved[key],
|
|
782
|
+
FieldInst.new(FieldStatus.UNCERTAIN))
|
|
783
|
+
|
|
784
|
+
if self.solved is not None:
|
|
785
|
+
for key, v in self.solved.items():
|
|
786
|
+
if key in handled_fields:
|
|
787
|
+
continue
|
|
788
|
+
handle_field(key, v, FieldInst.new(FieldStatus.SOLVED))
|
|
789
|
+
|
|
790
|
+
for key, field in self.config._fields.items():
|
|
791
|
+
if key in handled_fields:
|
|
792
|
+
continue
|
|
793
|
+
handle_field(key, "", FieldInst.new(FieldStatus.UNSOLVED))
|
|
794
|
+
return pd.DataFrame(rows)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from z3 import *
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from pydantic.config import ConfigDict
|
|
4
|
+
from typing import Type, TypedDict, Never
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ArbTypesModel(BaseModel):
|
|
8
|
+
model_config: ConfigDict = ConfigDict(arbitrary_types_allowed=True)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def free_vars(expr):
|
|
12
|
+
s = set()
|
|
13
|
+
stack = [expr]
|
|
14
|
+
while stack:
|
|
15
|
+
e = stack.pop()
|
|
16
|
+
if is_const(e) and e.decl().kind() == Z3_OP_UNINTERPRETED:
|
|
17
|
+
s.add(e)
|
|
18
|
+
for c in e.children():
|
|
19
|
+
stack.append(c)
|
|
20
|
+
return s
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SimpleDatatypes(TypedDict):
|
|
24
|
+
int: ArithSortRef
|
|
25
|
+
float: FPSortRef
|
|
26
|
+
bool: BoolSortRef
|
|
27
|
+
str: SeqSortRef
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_simple_datatypes() -> SimpleDatatypes:
|
|
31
|
+
return {
|
|
32
|
+
int: IntSort(),
|
|
33
|
+
float: Float64(),
|
|
34
|
+
bool: BoolSort(),
|
|
35
|
+
str: StringSort(),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OptDatatypes(TypedDict):
|
|
40
|
+
int: DatatypeSortRef
|
|
41
|
+
float: DatatypeSortRef
|
|
42
|
+
bool: DatatypeSortRef
|
|
43
|
+
str: DatatypeSortRef
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_optional_datatypes(simple_types: SimpleDatatypes) -> OptDatatypes:
|
|
47
|
+
|
|
48
|
+
def _build_opt_dt(raw_sort: z3.SortRef, typename: str) -> DatatypeSortRef:
|
|
49
|
+
NewType = Datatype('Opt' + typename)
|
|
50
|
+
NewType.declare('Nil')
|
|
51
|
+
NewType.declare('Some', ('val', raw_sort))
|
|
52
|
+
return NewType.create()
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
py_type: _build_opt_dt(z3_raw_sort, py_type.__name__.capitalize())
|
|
56
|
+
for py_type, z3_raw_sort in simple_types.items()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
SimpleTypesZ3 = get_simple_datatypes()
|
|
61
|
+
OptTypesZ3 = get_optional_datatypes(SimpleTypesZ3)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from pydantic.config import ConfigDict
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from textual.coordinate import Coordinate
|
|
5
|
+
from textual.app import App, ComposeResult
|
|
6
|
+
from textual.widgets import Header, Footer, DataTable, Static, TabbedContent, TabPane, Label
|
|
7
|
+
from textual.containers import Vertical, VerticalScroll
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
from rich.style import Style
|
|
10
|
+
from typing import Self, Optional
|
|
11
|
+
|
|
12
|
+
from smtconfig.utils import free_vars
|
|
13
|
+
|
|
14
|
+
from .config import ConfigInstance, FieldInst, FieldStatus, Rule, SMTResult
|
|
15
|
+
from icecream import ic
|
|
16
|
+
|
|
17
|
+
STATUS_COLORS = {
|
|
18
|
+
FieldStatus.PROVIDED: "cyan",
|
|
19
|
+
FieldStatus.SOLVED: "green",
|
|
20
|
+
FieldStatus.CONFLICT: "red",
|
|
21
|
+
FieldStatus.UNCERTAIN: "yellow",
|
|
22
|
+
FieldStatus.UNSOLVED: "magenta",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Label.link_style = Style(underline=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ArbTypesModel(BaseModel):
|
|
29
|
+
model_config: ConfigDict = ConfigDict(arbitrary_types_allowed=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ViewerDocument(ArbTypesModel):
|
|
33
|
+
name: str
|
|
34
|
+
config_inst: ConfigInstance
|
|
35
|
+
df: pd.DataFrame
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def prep_document(cls, name: str, config_inst: ConfigInstance) -> Self:
|
|
39
|
+
df = config_inst.build_table_dataframe()
|
|
40
|
+
return cls(name=name, config_inst=config_inst, df=df)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ConfigViewer(App):
|
|
44
|
+
CSS = """
|
|
45
|
+
#status {
|
|
46
|
+
padding: 1;
|
|
47
|
+
height: 3;
|
|
48
|
+
}
|
|
49
|
+
#bottom-pane {
|
|
50
|
+
dock: bottom;
|
|
51
|
+
height: 50%;
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self,
|
|
56
|
+
config_inst: ConfigInstance,
|
|
57
|
+
title: Optional[str] = None):
|
|
58
|
+
super().__init__()
|
|
59
|
+
self.title = title
|
|
60
|
+
self.sub_title = "ConfigViewer"
|
|
61
|
+
self.config_inst = config_inst
|
|
62
|
+
self.status_header = Static("kek", markup=True, id="status")
|
|
63
|
+
self.df = self.config_inst.build_table_dataframe()
|
|
64
|
+
|
|
65
|
+
def compose(self) -> ComposeResult:
|
|
66
|
+
yield Header()
|
|
67
|
+
yield Footer()
|
|
68
|
+
with Vertical():
|
|
69
|
+
yield self.status_header
|
|
70
|
+
with VerticalScroll():
|
|
71
|
+
yield DataTable(id="fields",
|
|
72
|
+
cursor_type="row") # self.field_table
|
|
73
|
+
with TabbedContent(id="bottom-pane"):
|
|
74
|
+
with TabPane("Rules", id="rules-pane"):
|
|
75
|
+
yield Label("Rules", markup=True, id="rules")
|
|
76
|
+
with TabPane("Versions", id="versions-pane"):
|
|
77
|
+
yield Static("Versions", markup=True, id="versions")
|
|
78
|
+
with TabPane("Logs", id="logs-pane"):
|
|
79
|
+
yield Static("Logs", markup=True, id="logs")
|
|
80
|
+
|
|
81
|
+
def _status_markup(self):
|
|
82
|
+
s = self.config_inst.status
|
|
83
|
+
if s.sat():
|
|
84
|
+
return f"Status: [green]{s}[/green]"
|
|
85
|
+
else:
|
|
86
|
+
return f"Status: [red]{s}[/red]"
|
|
87
|
+
|
|
88
|
+
def _fmt_field_status(self, st: FieldInst):
|
|
89
|
+
c = STATUS_COLORS.get(st.status, "white")
|
|
90
|
+
return f"[{c}]{str(st)}[/{c}]"
|
|
91
|
+
|
|
92
|
+
def _find_field_row(self, field_name: str):
|
|
93
|
+
data = self.df[self.df["Field"] == field_name]
|
|
94
|
+
if len(data) == 0:
|
|
95
|
+
return None
|
|
96
|
+
assert len(data) == 1
|
|
97
|
+
return next(iter(data.itertuples(index=False)))
|
|
98
|
+
|
|
99
|
+
def on_mount(self):
|
|
100
|
+
field_table = self.query_one(DataTable)
|
|
101
|
+
field_table.add_columns("Field", "Value", "Type", "Status")
|
|
102
|
+
for row in self.df.itertuples():
|
|
103
|
+
field_table.add_row(row.Field, str(row.Value), row.Type,
|
|
104
|
+
self._fmt_field_status(row.Status))
|
|
105
|
+
self.status_header.content = self._status_markup()
|
|
106
|
+
|
|
107
|
+
def _format_rule_expr(self, rule: Rule) -> Text:
|
|
108
|
+
rule_expr = str(rule.expr)
|
|
109
|
+
fields = [str(var) for var in free_vars(rule.expr)]
|
|
110
|
+
out = Text()
|
|
111
|
+
i = 0
|
|
112
|
+
n = len(rule_expr)
|
|
113
|
+
|
|
114
|
+
fields_sorted = sorted(fields, key=len, reverse=True)
|
|
115
|
+
|
|
116
|
+
while i < n:
|
|
117
|
+
matched = False
|
|
118
|
+
for f in fields_sorted:
|
|
119
|
+
if rule_expr.startswith(f, i):
|
|
120
|
+
# raise Exception(
|
|
121
|
+
# STATUS_COLORS[self._find_field_row(f).Status.status])
|
|
122
|
+
if not self._find_field_row(f):
|
|
123
|
+
continue
|
|
124
|
+
seg = Text(
|
|
125
|
+
f,
|
|
126
|
+
STATUS_COLORS[self._find_field_row(f).Status.status])
|
|
127
|
+
seg = seg.on(click=f"app.highlight_field('{f}')")
|
|
128
|
+
out.append(seg)
|
|
129
|
+
i += len(f)
|
|
130
|
+
matched = True
|
|
131
|
+
break
|
|
132
|
+
if not matched:
|
|
133
|
+
out.append(rule_expr[i])
|
|
134
|
+
i += 1
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
def action_highlight_field(self, field_name: str):
|
|
138
|
+
idx = self.df.index[self.df["Field"] == field_name].tolist()
|
|
139
|
+
assert len(idx) == 1
|
|
140
|
+
idx = idx[0]
|
|
141
|
+
field_table = self.query_one(DataTable)
|
|
142
|
+
field_table.cursor_coordinate = Coordinate(row=idx, column=0)
|
|
143
|
+
# field_table.
|
|
144
|
+
|
|
145
|
+
def _refresh_rules(self, new_field: int):
|
|
146
|
+
rules_info: Label = self.query_one("#rules", Label)
|
|
147
|
+
# self.app.highligh_field("EEEEEEEe")
|
|
148
|
+
field_name = self.df.loc[new_field]["Field"]
|
|
149
|
+
rules = self.config_inst.config.rules_on_fields([field_name])
|
|
150
|
+
|
|
151
|
+
def _get_rule_expr(rule: Rule):
|
|
152
|
+
parts = [Text(rule.name), self._format_rule_expr(rule)]
|
|
153
|
+
res = Text(": ").join(parts)
|
|
154
|
+
return res
|
|
155
|
+
|
|
156
|
+
if len(rules) > 0:
|
|
157
|
+
rule_exprs = [_get_rule_expr(rule) for rule in rules]
|
|
158
|
+
rules_info.content = (
|
|
159
|
+
Text(f"'{field_name}' is involved in rules:\n", ) +
|
|
160
|
+
Text('\n').join(
|
|
161
|
+
Text(f'{i}) ') + expr
|
|
162
|
+
for i, expr in enumerate(rule_exprs, start=1)))
|
|
163
|
+
else:
|
|
164
|
+
rules_info.content = f"'{field_name}' is not involved in any rules"
|
|
165
|
+
|
|
166
|
+
def on_key(self, event):
|
|
167
|
+
field_table = self.query_one(DataTable)
|
|
168
|
+
if event.key in ("q", ):
|
|
169
|
+
self.exit()
|
|
170
|
+
elif event.key == "j":
|
|
171
|
+
field_table.action_cursor_down()
|
|
172
|
+
elif event.key == "k":
|
|
173
|
+
field_table.action_cursor_up()
|
|
174
|
+
|
|
175
|
+
def on_data_table_row_highlighted(self,
|
|
176
|
+
event: DataTable.RowHighlighted) -> None:
|
|
177
|
+
match event.data_table.id:
|
|
178
|
+
case "fields":
|
|
179
|
+
self._refresh_rules(event.cursor_row)
|
|
180
|
+
case _:
|
|
181
|
+
raise Exception(
|
|
182
|
+
f"Unexpected datatable '{event.data_table.id}'")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smtconfig
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Declarative SMT-based configuration system built on top of Z3
|
|
5
|
+
Author: Nikolai Krivoshchapov
|
|
6
|
+
License: BSD 3-Clause License
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: z3-solver
|
|
11
|
+
Requires-Dist: pydantic
|
|
12
|
+
Requires-Dist: networkx
|
|
13
|
+
Requires-Dist: pandas
|
|
14
|
+
Requires-Dist: rich
|
|
15
|
+
Requires-Dist: textual
|
|
16
|
+
Requires-Dist: icecream
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
## SMTConfig
|
|
20
|
+
|
|
21
|
+
Configuration validation and completion using z3 solver.
|
|
22
|
+
|
|
23
|
+
### Installation
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
pip install smtconfig
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Example
|
|
30
|
+
|
|
31
|
+
[see here](https://gitlab.com/knvvv/smtconfig/-/blob/master/example.py)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
./smtconfig/__init__.py
|
|
5
|
+
./smtconfig/config.py
|
|
6
|
+
./smtconfig/utils.py
|
|
7
|
+
./smtconfig/vis.py
|
|
8
|
+
smtconfig/__init__.py
|
|
9
|
+
smtconfig/config.py
|
|
10
|
+
smtconfig/utils.py
|
|
11
|
+
smtconfig/vis.py
|
|
12
|
+
smtconfig.egg-info/PKG-INFO
|
|
13
|
+
smtconfig.egg-info/SOURCES.txt
|
|
14
|
+
smtconfig.egg-info/dependency_links.txt
|
|
15
|
+
smtconfig.egg-info/requires.txt
|
|
16
|
+
smtconfig.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
smtconfig
|