speclogician 0.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- speclogician/__init__.py +0 -0
- speclogician/commands/__init__.py +15 -0
- speclogician/commands/cmd_ch.py +616 -0
- speclogician/commands/cmd_find.py +256 -0
- speclogician/commands/cmd_view.py +202 -0
- speclogician/commands/runner.py +149 -0
- speclogician/commands/utils.py +101 -0
- speclogician/data/__init__.py +0 -0
- speclogician/data/artifact.py +63 -0
- speclogician/data/container.py +402 -0
- speclogician/data/mapping.py +88 -0
- speclogician/data/refs.py +24 -0
- speclogician/data/traces.py +26 -0
- speclogician/demos/.DS_Store +0 -0
- speclogician/demos/cmd_demo.py +278 -0
- speclogician/demos/loader.py +135 -0
- speclogician/demos/model.py +27 -0
- speclogician/demos/runner.py +51 -0
- speclogician/logic/__init__.py +11 -0
- speclogician/logic/api/__init__.py +29 -0
- speclogician/logic/api/client.py +606 -0
- speclogician/logic/api/decomp.py +67 -0
- speclogician/logic/api/scenario.py +102 -0
- speclogician/logic/api/traces.py +59 -0
- speclogician/logic/lib/__init__.py +19 -0
- speclogician/logic/lib/complement.py +107 -0
- speclogician/logic/lib/domain_model.py +59 -0
- speclogician/logic/lib/predicates.py +151 -0
- speclogician/logic/lib/scenarios.py +369 -0
- speclogician/logic/lib/traces.py +114 -0
- speclogician/logic/lib/transitions.py +104 -0
- speclogician/logic/main.py +246 -0
- speclogician/logic/strings.py +194 -0
- speclogician/logic/utils.py +135 -0
- speclogician/main.py +139 -0
- speclogician/modeling/__init__.py +31 -0
- speclogician/modeling/complement.py +104 -0
- speclogician/modeling/component.py +71 -0
- speclogician/modeling/conflict.py +26 -0
- speclogician/modeling/domain.py +349 -0
- speclogician/modeling/predicates.py +59 -0
- speclogician/modeling/scenario.py +162 -0
- speclogician/modeling/spec.py +306 -0
- speclogician/modeling/spec_stats.py +39 -0
- speclogician/presentation/__init__.py +0 -0
- speclogician/presentation/api.py +244 -0
- speclogician/presentation/builders/__init__.py +0 -0
- speclogician/presentation/builders/_links.py +44 -0
- speclogician/presentation/builders/container.py +53 -0
- speclogician/presentation/builders/data_artifact.py +42 -0
- speclogician/presentation/builders/domain.py +54 -0
- speclogician/presentation/builders/instances_list.py +38 -0
- speclogician/presentation/builders/predicate.py +51 -0
- speclogician/presentation/builders/recommendations.py +41 -0
- speclogician/presentation/builders/scenario.py +41 -0
- speclogician/presentation/builders/scenario_complement.py +82 -0
- speclogician/presentation/builders/smart_find.py +39 -0
- speclogician/presentation/builders/spec.py +39 -0
- speclogician/presentation/builders/state_diff.py +150 -0
- speclogician/presentation/builders/state_instance.py +42 -0
- speclogician/presentation/builders/state_instance_summary.py +84 -0
- speclogician/presentation/builders/trace.py +58 -0
- speclogician/presentation/ctx.py +38 -0
- speclogician/presentation/models/__init__.py +0 -0
- speclogician/presentation/models/container.py +44 -0
- speclogician/presentation/models/data_artifact.py +33 -0
- speclogician/presentation/models/domain.py +50 -0
- speclogician/presentation/models/instances_list.py +23 -0
- speclogician/presentation/models/predicate.py +60 -0
- speclogician/presentation/models/recommendations.py +34 -0
- speclogician/presentation/models/scenario.py +31 -0
- speclogician/presentation/models/scenario_complement.py +40 -0
- speclogician/presentation/models/smart_find.py +34 -0
- speclogician/presentation/models/spec.py +32 -0
- speclogician/presentation/models/state_diff.py +34 -0
- speclogician/presentation/models/state_instance.py +31 -0
- speclogician/presentation/models/state_instance_summary.py +102 -0
- speclogician/presentation/models/trace.py +42 -0
- speclogician/presentation/preview/__init__.py +13 -0
- speclogician/presentation/preview/cli.py +50 -0
- speclogician/presentation/preview/fixtures/__init__.py +205 -0
- speclogician/presentation/preview/fixtures/artifact_container.py +150 -0
- speclogician/presentation/preview/fixtures/data_artifact.py +144 -0
- speclogician/presentation/preview/fixtures/domain_model.py +162 -0
- speclogician/presentation/preview/fixtures/instances_list.py +162 -0
- speclogician/presentation/preview/fixtures/predicate.py +184 -0
- speclogician/presentation/preview/fixtures/scenario.py +84 -0
- speclogician/presentation/preview/fixtures/scenario_complement.py +81 -0
- speclogician/presentation/preview/fixtures/smart_find.py +140 -0
- speclogician/presentation/preview/fixtures/spec.py +95 -0
- speclogician/presentation/preview/fixtures/state_diff.py +158 -0
- speclogician/presentation/preview/fixtures/state_instance.py +128 -0
- speclogician/presentation/preview/fixtures/state_instance_summary.py +80 -0
- speclogician/presentation/preview/fixtures/trace.py +206 -0
- speclogician/presentation/preview/registry.py +42 -0
- speclogician/presentation/renderers/__init__.py +24 -0
- speclogician/presentation/renderers/container.py +136 -0
- speclogician/presentation/renderers/data_artifact.py +144 -0
- speclogician/presentation/renderers/domain.py +123 -0
- speclogician/presentation/renderers/instances_list.py +120 -0
- speclogician/presentation/renderers/predicate.py +180 -0
- speclogician/presentation/renderers/recommendations.py +90 -0
- speclogician/presentation/renderers/scenario.py +94 -0
- speclogician/presentation/renderers/scenario_complement.py +59 -0
- speclogician/presentation/renderers/smart_find.py +307 -0
- speclogician/presentation/renderers/spec.py +105 -0
- speclogician/presentation/renderers/state_diff.py +102 -0
- speclogician/presentation/renderers/state_instance.py +82 -0
- speclogician/presentation/renderers/state_instance_summary.py +143 -0
- speclogician/presentation/renderers/trace.py +122 -0
- speclogician/py.typed +0 -0
- speclogician/shell/app.py +170 -0
- speclogician/shell/shell_ch.py +263 -0
- speclogician/shell/shell_view.py +153 -0
- speclogician/state/__init__.py +0 -0
- speclogician/state/change.py +428 -0
- speclogician/state/change_result.py +32 -0
- speclogician/state/diff.py +191 -0
- speclogician/state/inst.py +574 -0
- speclogician/state/recommendation.py +13 -0
- speclogician/state/recommender.py +577 -0
- speclogician/state/state.py +465 -0
- speclogician/state/state_stats.py +133 -0
- speclogician/tui/__init__.py +0 -0
- speclogician/tui/app.py +257 -0
- speclogician/tui/app.tcss +160 -0
- speclogician/tui/demo.py +45 -0
- speclogician/tui/images/speclogician-full.png +0 -0
- speclogician/tui/images/speclogician-minimal.png +0 -0
- speclogician/tui/main_screen.py +454 -0
- speclogician/tui/splash_screen.py +51 -0
- speclogician/tui/stats_screen.py +125 -0
- speclogician/utils/__init__.py +78 -0
- speclogician/utils/load.py +166 -0
- speclogician/utils/prompt.md +325 -0
- speclogician/utils/testing.py +151 -0
- speclogician-0.0.0b1.dist-info/METADATA +116 -0
- speclogician-0.0.0b1.dist-info/RECORD +139 -0
- speclogician-0.0.0b1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/modeling/domain.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from typing import Sequence
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from .predicates import (
|
|
11
|
+
StatePredicate, ActionPredicate, Transition, PredicateType
|
|
12
|
+
)
|
|
13
|
+
from .__init__ import extract_declared_types
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
|
|
16
|
+
class BaseStatus(StrEnum):
|
|
17
|
+
UNKNOWN = "unknown"
|
|
18
|
+
INVALID_IML = "invalid_iml"
|
|
19
|
+
VALID = "valid"
|
|
20
|
+
|
|
21
|
+
class DomainModel(BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
The domain model (base model with state/action types + other auxillary functions,
|
|
24
|
+
and predicates/transition functions)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
base_status : BaseStatus = BaseStatus.UNKNOWN # IML status
|
|
28
|
+
base_has_state : bool = False # Model contains `state` type definition
|
|
29
|
+
base_has_action : bool = False # Model contains `action` type definition
|
|
30
|
+
base : str = "" # Includes type definitions and other
|
|
31
|
+
|
|
32
|
+
transitions : list[Transition] = Field(default_factory=list[Transition]) # 'state * action -> state' tansition functions
|
|
33
|
+
state_preds : list[StatePredicate] = Field(default_factory=list[StatePredicate]) # 'state -> bool' predicates
|
|
34
|
+
action_preds : list[ActionPredicate] = Field(default_factory=list[ActionPredicate]) # 'state * action -> bool' predicates
|
|
35
|
+
|
|
36
|
+
# transitions
|
|
37
|
+
num_trans_total : int = 0
|
|
38
|
+
num_trans_matched : int = 0
|
|
39
|
+
num_trans_valid_logic : int = 0
|
|
40
|
+
num_trans_errored : int = 0
|
|
41
|
+
|
|
42
|
+
# state-only predicates
|
|
43
|
+
num_state_preds_total : int = 0
|
|
44
|
+
num_state_preds_matched : int = 0
|
|
45
|
+
num_state_preds_valid_logic : int = 0
|
|
46
|
+
num_state_preds_errored : int = 0
|
|
47
|
+
|
|
48
|
+
# state x action predicates
|
|
49
|
+
num_action_preds_total : int = 0
|
|
50
|
+
num_action_preds_matched : int = 0
|
|
51
|
+
num_action_preds_valid_logic : int = 0
|
|
52
|
+
num_action_preds_errored : int = 0
|
|
53
|
+
|
|
54
|
+
# predicate totals
|
|
55
|
+
num_preds_total : int = 0
|
|
56
|
+
num_preds_matched : int = 0
|
|
57
|
+
num_preds_valid_logic : int = 0
|
|
58
|
+
num_preds_errored : int = 0
|
|
59
|
+
|
|
60
|
+
def assert_has_predicates(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
given: Sequence[str],
|
|
64
|
+
when: Sequence[str],
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Ensure all predicates referenced by a scenario exist in the domain model.
|
|
67
|
+
|
|
68
|
+
- `given` must reference state predicates (state -> bool)
|
|
69
|
+
- `when` must reference action predicates (state -> action -> bool)
|
|
70
|
+
|
|
71
|
+
Raises ValueError with a helpful message if anything is missing or mismatched.
|
|
72
|
+
"""
|
|
73
|
+
missing_state: list[str] = []
|
|
74
|
+
missing_action: list[str] = []
|
|
75
|
+
wrong_kind_given: list[str] = []
|
|
76
|
+
wrong_kind_when: list[str] = []
|
|
77
|
+
|
|
78
|
+
for name in given:
|
|
79
|
+
p = self.get_pred_by_name(name)
|
|
80
|
+
if p is None:
|
|
81
|
+
missing_state.append(name)
|
|
82
|
+
elif p not in self.state_preds:
|
|
83
|
+
# Exists, but it's an action predicate (or otherwise not a state pred)
|
|
84
|
+
wrong_kind_given.append(name)
|
|
85
|
+
|
|
86
|
+
for name in when:
|
|
87
|
+
p = self.get_pred_by_name(name)
|
|
88
|
+
if p is None:
|
|
89
|
+
missing_action.append(name)
|
|
90
|
+
elif p not in self.action_preds:
|
|
91
|
+
# Exists, but it's a state predicate (or otherwise not an action pred)
|
|
92
|
+
wrong_kind_when.append(name)
|
|
93
|
+
|
|
94
|
+
if missing_state or missing_action or wrong_kind_given or wrong_kind_when:
|
|
95
|
+
parts: list[str] = []
|
|
96
|
+
if missing_state:
|
|
97
|
+
parts.append(f"missing state predicates (given): {missing_state}")
|
|
98
|
+
if wrong_kind_given:
|
|
99
|
+
parts.append(f"not state predicates (given): {wrong_kind_given}")
|
|
100
|
+
if missing_action:
|
|
101
|
+
parts.append(f"missing action predicates (when): {missing_action}")
|
|
102
|
+
if wrong_kind_when:
|
|
103
|
+
parts.append(f"not action predicates (when): {wrong_kind_when}")
|
|
104
|
+
|
|
105
|
+
raise ValueError("Scenario references invalid predicates: " + "; ".join(parts))
|
|
106
|
+
|
|
107
|
+
def assert_has_transitions(self, *, then: Sequence[str]) -> None:
|
|
108
|
+
"""Ensure all transitions referenced by a scenario exist in the domain model.
|
|
109
|
+
|
|
110
|
+
`then` is a list of transition function names.
|
|
111
|
+
|
|
112
|
+
Raises ValueError with a helpful message if anything is missing.
|
|
113
|
+
"""
|
|
114
|
+
missing: list[str] = []
|
|
115
|
+
|
|
116
|
+
for name in then:
|
|
117
|
+
t = self.get_trans_by_name(name)
|
|
118
|
+
if t is None:
|
|
119
|
+
missing.append(name)
|
|
120
|
+
|
|
121
|
+
if missing:
|
|
122
|
+
raise ValueError(f"Scenario references unknown transitions (then): {missing}")
|
|
123
|
+
|
|
124
|
+
def get_pred_by_name (self, name:str) -> StatePredicate|ActionPredicate|None:
|
|
125
|
+
""" Return a predicate body for the specified name """
|
|
126
|
+
s_pred = next((p for p in self.state_preds if p.name == name), None)
|
|
127
|
+
|
|
128
|
+
if s_pred is None:
|
|
129
|
+
return next((p for p in self.action_preds if p.name == name), None)
|
|
130
|
+
else:
|
|
131
|
+
return s_pred
|
|
132
|
+
|
|
133
|
+
def get_trans_by_name (self, name:str) -> Transition|None:
|
|
134
|
+
""" Return transition body for a specified name """
|
|
135
|
+
return next((p for p in self.transitions if p.name == name), None)
|
|
136
|
+
|
|
137
|
+
def action_pred_names(self) -> list[str]:
|
|
138
|
+
""" List of action predicate names """
|
|
139
|
+
return list(map(lambda x: x.name, self.action_preds))
|
|
140
|
+
|
|
141
|
+
def state_pred_names(self) -> list[str]:
|
|
142
|
+
""" List of available state predicate names """
|
|
143
|
+
return list(map(lambda x: x.name, self.state_preds))
|
|
144
|
+
|
|
145
|
+
def transition_names(self) -> list[str]:
|
|
146
|
+
""" List of available transitions """
|
|
147
|
+
return list(map(lambda x: x.name, self.transitions))
|
|
148
|
+
|
|
149
|
+
def pred_exists(self, name:str) -> bool:
|
|
150
|
+
"""
|
|
151
|
+
Does this predicate exist?
|
|
152
|
+
"""
|
|
153
|
+
if next ((p for p in self.state_preds if p.name == name), None) is not None:
|
|
154
|
+
return True
|
|
155
|
+
return next ((p for p in self.action_preds if p.name == name), None) is not None
|
|
156
|
+
|
|
157
|
+
def add_predicate(self, name: str, ptype: PredicateType, src_code: str) -> None:
|
|
158
|
+
"""Add a new predicate to the domain model."""
|
|
159
|
+
|
|
160
|
+
# Enforce global uniqueness across all predicates
|
|
161
|
+
if any(p.name == name for p in self.state_preds) or \
|
|
162
|
+
any(p.name == name for p in self.action_preds):
|
|
163
|
+
raise ValueError(f"Predicate '{name}' already exists in domain model")
|
|
164
|
+
|
|
165
|
+
if ptype == PredicateType.STATE:
|
|
166
|
+
pred = StatePredicate(
|
|
167
|
+
name=name,
|
|
168
|
+
src_code=src_code,
|
|
169
|
+
)
|
|
170
|
+
self.state_preds.append(pred)
|
|
171
|
+
|
|
172
|
+
elif ptype == PredicateType.ACTION:
|
|
173
|
+
pred = ActionPredicate(
|
|
174
|
+
name=name,
|
|
175
|
+
src_code=src_code,
|
|
176
|
+
)
|
|
177
|
+
self.action_preds.append(pred)
|
|
178
|
+
|
|
179
|
+
else:
|
|
180
|
+
raise ValueError(f"Attempting to add unknown type of predicate: {ptype}")
|
|
181
|
+
|
|
182
|
+
def edit_predicate(self, name: str, src_code: str) -> None:
|
|
183
|
+
"""Edit a predicate by name and update last_updated."""
|
|
184
|
+
|
|
185
|
+
matches = [p for p in self.state_preds if p.name == name] + \
|
|
186
|
+
[p for p in self.action_preds if p.name == name]
|
|
187
|
+
|
|
188
|
+
if not matches:
|
|
189
|
+
raise ValueError(f"Predicate '{name}' not found")
|
|
190
|
+
|
|
191
|
+
if len(matches) > 1:
|
|
192
|
+
raise ValueError(f"Predicate '{name}' is ambiguous (found {len(matches)} matches)")
|
|
193
|
+
|
|
194
|
+
pred = matches[0]
|
|
195
|
+
pred.set_src_code(src_code)
|
|
196
|
+
|
|
197
|
+
def rem_predicate(self, name: str) -> None:
|
|
198
|
+
"""Remove a predicate (state or action) by name."""
|
|
199
|
+
|
|
200
|
+
removed = False
|
|
201
|
+
|
|
202
|
+
# Remove from state predicates
|
|
203
|
+
for i, p in enumerate(self.state_preds):
|
|
204
|
+
if p.name == name:
|
|
205
|
+
del self.state_preds[i]
|
|
206
|
+
removed = True
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
# Remove from action predicates
|
|
210
|
+
for i, p in enumerate(self.action_preds):
|
|
211
|
+
if p.name == name:
|
|
212
|
+
del self.action_preds[i]
|
|
213
|
+
removed = True
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
if not removed:
|
|
217
|
+
raise ValueError(f"Predicate '{name}' not found in domain model")
|
|
218
|
+
|
|
219
|
+
def add_transition(self, name: str, src_code: str) -> None:
|
|
220
|
+
"""Add a new transition function to the domain model."""
|
|
221
|
+
|
|
222
|
+
# Ensure uniqueness
|
|
223
|
+
if any(t.name == name for t in self.transitions):
|
|
224
|
+
raise ValueError(f"Transition '{name}' already exists in domain model")
|
|
225
|
+
|
|
226
|
+
transition = Transition(
|
|
227
|
+
name=name,
|
|
228
|
+
src_code=src_code,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
self.transitions.append(transition)
|
|
232
|
+
|
|
233
|
+
def edit_transition(self, name: str, src_code: str) -> None:
|
|
234
|
+
"""Edit a transition function by name and update last_updated."""
|
|
235
|
+
matches = [t for t in self.transitions if t.name == name]
|
|
236
|
+
|
|
237
|
+
if not matches:
|
|
238
|
+
raise ValueError(f"Transition '{name}' not found in domain model")
|
|
239
|
+
|
|
240
|
+
if len(matches) > 1:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"Transition name '{name}' is ambiguous "
|
|
243
|
+
f"({len(matches)} transitions found)"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
transition = matches[0]
|
|
247
|
+
transition.set_src_code(src_code)
|
|
248
|
+
|
|
249
|
+
def rem_transition(self, name: str) -> None:
|
|
250
|
+
"""Remove a transition function by name."""
|
|
251
|
+
|
|
252
|
+
for i, t in enumerate(self.transitions):
|
|
253
|
+
if t.name == name:
|
|
254
|
+
del self.transitions[i]
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
raise ValueError(f"Transition '{name}' not found in domain model")
|
|
258
|
+
|
|
259
|
+
def trans_exists(self, name:str) -> bool:
|
|
260
|
+
""" Return True/False if transitions with specified name exists """
|
|
261
|
+
return next ((t for t in self.transitions if t.name == name), None) is not None
|
|
262
|
+
|
|
263
|
+
def state_specified (self) -> bool:
|
|
264
|
+
"""
|
|
265
|
+
Return True if the `state` type is properly specified
|
|
266
|
+
"""
|
|
267
|
+
return self.base_has_type('state')
|
|
268
|
+
|
|
269
|
+
def action_specified(self) -> bool:
|
|
270
|
+
"""
|
|
271
|
+
Return True if the `action` is properly specified
|
|
272
|
+
"""
|
|
273
|
+
return self.base_has_type('action')
|
|
274
|
+
|
|
275
|
+
def base_has_type(
|
|
276
|
+
self,
|
|
277
|
+
type_name : str
|
|
278
|
+
) -> bool:
|
|
279
|
+
"""
|
|
280
|
+
Check whether the base model has a specified type
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
declared_types = extract_declared_types(self.base)
|
|
284
|
+
return type_name in declared_types
|
|
285
|
+
|
|
286
|
+
def set_base (
|
|
287
|
+
self,
|
|
288
|
+
new_base : str) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Update the base model
|
|
291
|
+
"""
|
|
292
|
+
self.base = new_base
|
|
293
|
+
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
def make_iml_model (
|
|
297
|
+
self,
|
|
298
|
+
state_pred_names : list[str],
|
|
299
|
+
action_pred_names : list[str],
|
|
300
|
+
transition_names : list[str]
|
|
301
|
+
) -> str:
|
|
302
|
+
"""
|
|
303
|
+
Return base model with required predicates/transitions if needed
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
state_preds_str = ""
|
|
307
|
+
for p in state_pred_names:
|
|
308
|
+
pred = self.get_pred_by_name(p)
|
|
309
|
+
if pred is None:
|
|
310
|
+
continue
|
|
311
|
+
state_preds_str += f"{pred.to_iml()}"
|
|
312
|
+
if len(state_preds_str):
|
|
313
|
+
state_preds_str = f"(* State predicates *)\n{state_preds_str}"
|
|
314
|
+
|
|
315
|
+
action_preds_str = ""
|
|
316
|
+
for p in action_pred_names:
|
|
317
|
+
pred = self.get_pred_by_name(p)
|
|
318
|
+
if pred is None:
|
|
319
|
+
continue
|
|
320
|
+
action_preds_str += f"\n{pred.to_iml()}\n"
|
|
321
|
+
if len(action_preds_str):
|
|
322
|
+
action_preds_str = f"(* Action predicates *)\n{action_preds_str}"
|
|
323
|
+
|
|
324
|
+
trans_str = ""
|
|
325
|
+
for t in transition_names:
|
|
326
|
+
trans = self.get_trans_by_name(t)
|
|
327
|
+
if trans is None:
|
|
328
|
+
continue
|
|
329
|
+
trans_str += f"\n{trans.to_iml()}\n"
|
|
330
|
+
if len(trans_str):
|
|
331
|
+
trans_str = f"(* Transitions *)\n{trans_str}"
|
|
332
|
+
|
|
333
|
+
return f"""
|
|
334
|
+
(* Domain Model *)
|
|
335
|
+
|
|
336
|
+
(* Base *)
|
|
337
|
+
{self.base}
|
|
338
|
+
{state_preds_str}
|
|
339
|
+
{action_preds_str}
|
|
340
|
+
{trans_str}
|
|
341
|
+
""".rstrip("\n")
|
|
342
|
+
|
|
343
|
+
def to_iml(self):
|
|
344
|
+
"""
|
|
345
|
+
Create a consolidated IML model with all predicates and transitions
|
|
346
|
+
"""
|
|
347
|
+
return self.make_iml_model(
|
|
348
|
+
self.state_pred_names(), self.action_pred_names(), self.transition_names()
|
|
349
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/modeling/predicates.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
|
|
10
|
+
from .component import SrcComponent
|
|
11
|
+
|
|
12
|
+
class PredicateType(StrEnum):
|
|
13
|
+
STATE = "state"
|
|
14
|
+
ACTION = "action"
|
|
15
|
+
|
|
16
|
+
class StatePredicate(SrcComponent):
|
|
17
|
+
"""
|
|
18
|
+
Predicate on the state only
|
|
19
|
+
"""
|
|
20
|
+
kind: Literal["state_predicate"] = "state_predicate"
|
|
21
|
+
|
|
22
|
+
def __str__ (self):
|
|
23
|
+
""" """
|
|
24
|
+
return f"{self.name} s"
|
|
25
|
+
|
|
26
|
+
def to_iml (self):
|
|
27
|
+
""" Return the IML representation of the StatePredicate """
|
|
28
|
+
return f"let {self.name} (s : state) : bool = \n {self.src_code}\n"
|
|
29
|
+
|
|
30
|
+
class ActionPredicate(SrcComponent):
|
|
31
|
+
"""
|
|
32
|
+
Predicate involving a state and an action
|
|
33
|
+
"""
|
|
34
|
+
kind: Literal["action_predicate"] = "action_predicate"
|
|
35
|
+
|
|
36
|
+
def __str__(self):
|
|
37
|
+
"""
|
|
38
|
+
"""
|
|
39
|
+
return f"{self.name} s a"
|
|
40
|
+
|
|
41
|
+
def to_iml(self):
|
|
42
|
+
"""
|
|
43
|
+
Return the IML representation of the ActionPredicate
|
|
44
|
+
"""
|
|
45
|
+
return f"let {self.name} (s : state) (a : action) : bool = \n {self.src_code}\n"
|
|
46
|
+
|
|
47
|
+
class Transition(SrcComponent):
|
|
48
|
+
"""
|
|
49
|
+
Transition functions are used to update the state
|
|
50
|
+
"""
|
|
51
|
+
kind: Literal["transition"] = "transition"
|
|
52
|
+
def __str__ (self):
|
|
53
|
+
return f"{self.name} s a"
|
|
54
|
+
|
|
55
|
+
def to_iml (self):
|
|
56
|
+
"""
|
|
57
|
+
Return the IML representation of the transition
|
|
58
|
+
"""
|
|
59
|
+
return f"let {self.name} (s : state) (a : action) : state = \n {self.src_code}"
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/modeling/scenario.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TypeAlias, Sequence
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from .component import ModelComponent, validate_iml_identifier
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ScenarioDelta(BaseModel):
|
|
16
|
+
"""We'll use this to edit Scenarios."""
|
|
17
|
+
add: list[str] = Field(default_factory=list)
|
|
18
|
+
remove: list[str] = Field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
@field_validator("add", "remove")
|
|
21
|
+
@classmethod
|
|
22
|
+
def _dedup_and_strip(cls, xs: list[str]) -> list[str]:
|
|
23
|
+
# normalize, drop empties, dedup preserving order
|
|
24
|
+
out: list[str] = []
|
|
25
|
+
seen: set[str] = set()
|
|
26
|
+
for x in xs:
|
|
27
|
+
x = x.strip()
|
|
28
|
+
if not x or x in seen:
|
|
29
|
+
continue
|
|
30
|
+
out.append(x)
|
|
31
|
+
seen.add(x)
|
|
32
|
+
return out
|
|
33
|
+
|
|
34
|
+
@field_validator("add", "remove")
|
|
35
|
+
@classmethod
|
|
36
|
+
def _validate_identifiers(cls, xs: list[str]) -> list[str]:
|
|
37
|
+
# Reuse the canonical IML identifier validation
|
|
38
|
+
for x in xs:
|
|
39
|
+
validate_iml_identifier(x)
|
|
40
|
+
return xs
|
|
41
|
+
|
|
42
|
+
@model_validator(mode="after")
|
|
43
|
+
def _no_overlap(self) -> "ScenarioDelta":
|
|
44
|
+
overlap = set(self.add) & set(self.remove)
|
|
45
|
+
if overlap:
|
|
46
|
+
raise ValueError(f"Names cannot be both added and removed: {sorted(overlap)}")
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def is_empty(self) -> bool:
|
|
50
|
+
return not self.add and not self.remove
|
|
51
|
+
|
|
52
|
+
def is_add_only(self) -> bool:
|
|
53
|
+
return not self.remove
|
|
54
|
+
|
|
55
|
+
class Valid(BaseModel):
|
|
56
|
+
""" The scenario's valid """
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
class Inconsistent(BaseModel):
|
|
60
|
+
""" Scenario is inconsistent - the combination of predicates is unsatisfiable """
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
class Missing(BaseModel):
|
|
64
|
+
""" Referenced model components are missing """
|
|
65
|
+
missing_preds : list[str] = Field(default_factory=list)
|
|
66
|
+
missing_trans : list[str] = Field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
ComponentStatus : TypeAlias = Valid | Inconsistent | Missing
|
|
69
|
+
|
|
70
|
+
class Scenario(ModelComponent):
|
|
71
|
+
"""
|
|
72
|
+
Scenario contains
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name : str
|
|
76
|
+
|
|
77
|
+
# Predicates and transition functions are components
|
|
78
|
+
component_status : ComponentStatus = Valid()
|
|
79
|
+
|
|
80
|
+
given: Sequence[str] = Field(default_factory=list)
|
|
81
|
+
when: Sequence[str] = Field(default_factory=list)
|
|
82
|
+
then: Sequence[str] = Field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# When we check the DomainModel and this is missing,
|
|
86
|
+
# we'll then set these fields
|
|
87
|
+
preds_missing : list[str] = Field(default_factory=list)
|
|
88
|
+
trans_missing : list[str] = Field(default_factory=list)
|
|
89
|
+
|
|
90
|
+
# These contain consistency checks
|
|
91
|
+
# why precisely they're inconsistent
|
|
92
|
+
given_preds_consistent : bool = True
|
|
93
|
+
when_preds_consistent : bool = True
|
|
94
|
+
all_preds_consistent : bool = True
|
|
95
|
+
|
|
96
|
+
def pred_calls_to_iml (self) -> str:
|
|
97
|
+
"""
|
|
98
|
+
return &&'ed string of all predicates
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
given_str = "\n&& ".join(map(lambda x: f"{x} s", self.given))
|
|
102
|
+
when_str = "\n&& ".join(map(lambda x: f"{x} s a", self.when))
|
|
103
|
+
|
|
104
|
+
match (len(self.given), len(self.when)):
|
|
105
|
+
case (0, 0):
|
|
106
|
+
return ""
|
|
107
|
+
case (_, 0):
|
|
108
|
+
return given_str
|
|
109
|
+
case (0, _):
|
|
110
|
+
return when_str
|
|
111
|
+
case (_, _):
|
|
112
|
+
return f"{given_str} \n&& {when_str}"
|
|
113
|
+
|
|
114
|
+
def eval_transition (self, state_val:str, act_val:str) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Given concrete state and action values, evaluate the set of transitions
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
if len(self.then) == 0:
|
|
120
|
+
return state_val
|
|
121
|
+
|
|
122
|
+
trans_iml = ""
|
|
123
|
+
for t in self.then:
|
|
124
|
+
trans_iml += f"\nlet s = {t} s a in"
|
|
125
|
+
|
|
126
|
+
trans_iml += "\n"
|
|
127
|
+
|
|
128
|
+
return ''
|
|
129
|
+
|
|
130
|
+
def preds_to_iml (self):
|
|
131
|
+
"""
|
|
132
|
+
Return IML code of just the predicates (both types) encoded in IML
|
|
133
|
+
"""
|
|
134
|
+
given_str = "&& \n".join(map(str, self.given))
|
|
135
|
+
when_str = "&& \n".join(map(str, self.when))
|
|
136
|
+
|
|
137
|
+
s = f"""
|
|
138
|
+
{given_str}
|
|
139
|
+
|
|
140
|
+
{when_str}
|
|
141
|
+
"""
|
|
142
|
+
return s
|
|
143
|
+
|
|
144
|
+
def full_model_to_iml(self):
|
|
145
|
+
"""
|
|
146
|
+
Create the full model (we omit the 'else' at the end because this is intended to be used with
|
|
147
|
+
other scenarios
|
|
148
|
+
"""
|
|
149
|
+
given_str = "&& \n".join(map(str, self.given))
|
|
150
|
+
when_str = "&& \n".join(map(str, self.when))
|
|
151
|
+
then_str = "\n".join(map(str, self.then))
|
|
152
|
+
|
|
153
|
+
s = f"""
|
|
154
|
+
(* Scenario: {self.name} *)
|
|
155
|
+
let given_true =
|
|
156
|
+
{given_str}
|
|
157
|
+
in let when_true =
|
|
158
|
+
{when_str}
|
|
159
|
+
in if given_true && when_true then
|
|
160
|
+
({then_str})
|
|
161
|
+
"""
|
|
162
|
+
return s
|