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,246 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/logic/main.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..modeling.spec import Spec
|
|
10
|
+
from ..data.container import ArtifactContainer
|
|
11
|
+
from ..data.mapping import ArtifactMap
|
|
12
|
+
|
|
13
|
+
from ..state.change import (
|
|
14
|
+
StateChange,
|
|
15
|
+
DomainModelBaseEdit,
|
|
16
|
+
PredicateAdd,
|
|
17
|
+
PredicateEdit,
|
|
18
|
+
PredicateRemove,
|
|
19
|
+
TransitionAdd,
|
|
20
|
+
TransitionEdit,
|
|
21
|
+
TransitionRemove,
|
|
22
|
+
ScenarioAdd,
|
|
23
|
+
ScenarioEdit,
|
|
24
|
+
ScenarioRemove,
|
|
25
|
+
AddTestTrace,
|
|
26
|
+
EditTestTrace,
|
|
27
|
+
AddLogTrace,
|
|
28
|
+
EditLogTrace
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from .lib.domain_model import check_domain_model
|
|
32
|
+
from .lib.predicates import check_predicates
|
|
33
|
+
from .lib.transitions import check_transitions
|
|
34
|
+
from .lib.scenarios import check_scenarios
|
|
35
|
+
from .lib.traces import check_traces
|
|
36
|
+
from .lib.complement import maybe_update_spec_complement
|
|
37
|
+
|
|
38
|
+
from ..utils import console
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _hdr(title: str, subtitle: str | None = None, json_only: bool = False) -> None:
|
|
42
|
+
if json_only:
|
|
43
|
+
return
|
|
44
|
+
from rich.panel import Panel
|
|
45
|
+
from rich.text import Text
|
|
46
|
+
|
|
47
|
+
console.print(
|
|
48
|
+
Panel(
|
|
49
|
+
Text(subtitle or ""),
|
|
50
|
+
title=f"[bold]Logician[/bold] · {title}",
|
|
51
|
+
border_style="cyan",
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _note(msg: str, json_only: bool = False) -> None:
|
|
57
|
+
if not json_only:
|
|
58
|
+
console.print(msg)
|
|
59
|
+
|
|
60
|
+
def analyze_change(
|
|
61
|
+
spec: Spec,
|
|
62
|
+
art_cont: ArtifactContainer,
|
|
63
|
+
art_map: ArtifactMap,
|
|
64
|
+
change: StateChange,
|
|
65
|
+
json_only: bool = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Top-level entry for logical analysis post-change event.
|
|
69
|
+
|
|
70
|
+
Only performs logic checks when relevant.
|
|
71
|
+
All other change types are treated as no-ops here.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# ----------------------------
|
|
75
|
+
# Domain base model changed
|
|
76
|
+
# ----------------------------
|
|
77
|
+
if isinstance(change, DomainModelBaseEdit):
|
|
78
|
+
_hdr("DomainModelBaseEdit", "Rechecking domain model and all dependent logic", json_only=json_only)
|
|
79
|
+
check_domain_model(spec, json_only=json_only)
|
|
80
|
+
check_predicates(spec, json_only=json_only)
|
|
81
|
+
check_transitions(spec, json_only=json_only)
|
|
82
|
+
check_scenarios(spec, json_only=json_only)
|
|
83
|
+
check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
maybe_update_spec_complement(spec)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
_note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
|
|
89
|
+
|
|
90
|
+
_note("[green]✓[/green] Domain analysis complete", json_only=json_only)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# ----------------------------
|
|
94
|
+
# Predicates changed
|
|
95
|
+
# ----------------------------
|
|
96
|
+
if isinstance(change, (PredicateAdd, PredicateEdit)):
|
|
97
|
+
_hdr("Predicate change", f"Predicate: [bold]{change.pred_name}[/bold]", json_only=json_only)
|
|
98
|
+
check_predicates(spec, names=[change.pred_name], json_only=json_only)
|
|
99
|
+
check_scenarios(spec, predicates=[change.pred_name], json_only=json_only)
|
|
100
|
+
check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
maybe_update_spec_complement(spec)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
_note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
|
|
106
|
+
|
|
107
|
+
_note("[green]✓[/green] Predicate analysis complete", json_only=json_only)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if isinstance(change, PredicateRemove):
|
|
111
|
+
_hdr("Predicate removed", f"[bold]{change.pred_name}[/bold]", json_only=json_only)
|
|
112
|
+
check_scenarios(spec, predicates=[change.pred_name], json_only=json_only)
|
|
113
|
+
check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
maybe_update_spec_complement(spec)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
_note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
|
|
119
|
+
|
|
120
|
+
_note("[green]✓[/green] Scenario refresh complete", json_only=json_only)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# ----------------------------
|
|
124
|
+
# Transitions changed
|
|
125
|
+
# ----------------------------
|
|
126
|
+
if isinstance(change, (TransitionAdd, TransitionEdit)):
|
|
127
|
+
_hdr("Transition change", f"Transition: [bold]{change.trans_name}[/bold]", json_only=json_only)
|
|
128
|
+
check_transitions(spec, names=[change.trans_name], json_only=json_only)
|
|
129
|
+
check_scenarios(spec, transitions=[change.trans_name], json_only=json_only)
|
|
130
|
+
check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
maybe_update_spec_complement(spec)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
_note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
|
|
136
|
+
|
|
137
|
+
_note("[green]✓[/green] Transition analysis complete", json_only=json_only)
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
if isinstance(change, TransitionRemove):
|
|
141
|
+
_hdr("Transition removed", f"[bold]{change.trans_name}[/bold]", json_only=json_only)
|
|
142
|
+
check_scenarios(spec, transitions=[change.trans_name], json_only=json_only)
|
|
143
|
+
check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
maybe_update_spec_complement(spec)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
_note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
|
|
149
|
+
|
|
150
|
+
_note("[green]✓[/green] Scenario refresh complete", json_only=json_only)
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# ----------------------------
|
|
154
|
+
# Scenarios changed
|
|
155
|
+
# ----------------------------
|
|
156
|
+
if isinstance(change, (ScenarioAdd, ScenarioEdit)):
|
|
157
|
+
_hdr("Scenario change", f"Scenario: [bold]{change.scenario_name}[/bold]", json_only=json_only)
|
|
158
|
+
check_scenarios(spec, names=[change.scenario_name], json_only=json_only)
|
|
159
|
+
check_traces(spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
maybe_update_spec_complement(spec)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
_note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
|
|
165
|
+
|
|
166
|
+
_note("[green]✓[/green] Scenario analysis complete", json_only=json_only)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
if isinstance(change, ScenarioRemove):
|
|
170
|
+
_hdr("Scenario removed", "Refreshing scenarios, predicates, and traces", json_only=json_only)
|
|
171
|
+
check_scenarios(spec, json_only=json_only)
|
|
172
|
+
check_predicates(spec, json_only=json_only)
|
|
173
|
+
check_traces(spec=spec, art_cont=art_cont, art_map=art_map, traces=[], json_only=json_only)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
maybe_update_spec_complement(spec)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
_note(f"[yellow]⚠[/yellow] Complement update failed (non-fatal): {type(e).__name__}: {e}", json_only=json_only)
|
|
179
|
+
|
|
180
|
+
_note("[green]✓[/green] Full refresh complete", json_only=json_only)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# ----------------------------
|
|
184
|
+
# Trace artifacts changed
|
|
185
|
+
# ----------------------------
|
|
186
|
+
if isinstance(change, AddTestTrace):
|
|
187
|
+
if not art_cont.test_traces:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
tt = art_cont.test_traces[-1]
|
|
191
|
+
# best-effort sanity check; no-op if mismatch
|
|
192
|
+
if change.art_id is not None and tt.art_id != change.art_id:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
assert tt.art_id is not None
|
|
196
|
+
|
|
197
|
+
check_traces(
|
|
198
|
+
spec=spec,
|
|
199
|
+
art_cont=art_cont,
|
|
200
|
+
art_map=art_map,
|
|
201
|
+
traces=[tt.art_id],
|
|
202
|
+
json_only=json_only,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
_note("[green]✓[/green] Trace analysis complete", json_only=json_only)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if isinstance(change, AddLogTrace):
|
|
209
|
+
if not art_cont.log_traces:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
lt = art_cont.log_traces[-1]
|
|
213
|
+
if change.art_id is not None and lt.art_id != change.art_id:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
assert lt.art_id is not None
|
|
217
|
+
|
|
218
|
+
check_traces(
|
|
219
|
+
spec=spec,
|
|
220
|
+
art_cont=art_cont,
|
|
221
|
+
art_map=art_map,
|
|
222
|
+
traces=[lt.art_id],
|
|
223
|
+
json_only=json_only,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
_note("[green]✓[/green] Trace analysis complete", json_only=json_only)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
if isinstance(change, (EditTestTrace, EditLogTrace)):
|
|
230
|
+
check_traces(
|
|
231
|
+
spec=spec,
|
|
232
|
+
art_cont=art_cont,
|
|
233
|
+
art_map=art_map,
|
|
234
|
+
traces=[change.art_id],
|
|
235
|
+
json_only=json_only,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
_note("[green]✓[/green] Trace analysis complete", json_only=json_only)
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
# ----------------------------
|
|
242
|
+
# Explicit no-op branch
|
|
243
|
+
# ----------------------------
|
|
244
|
+
# All remaining change types (data artifacts, mapping edits, etc.)
|
|
245
|
+
# are intentionally ignored here and handled elsewhere.
|
|
246
|
+
return
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/logic/strings.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Sequence
|
|
12
|
+
|
|
13
|
+
from ..modeling.scenario import Scenario
|
|
14
|
+
|
|
15
|
+
def parens(x: str) -> str:
|
|
16
|
+
x = x.strip()
|
|
17
|
+
return x if (x.startswith("(") and x.endswith(")")) else f"({x})"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def and_all(conjuncts: Sequence[str]) -> str:
|
|
21
|
+
if not conjuncts:
|
|
22
|
+
return "true"
|
|
23
|
+
return " && ".join(parens(c) for c in conjuncts)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def mk_predicates_only_instance_src(
|
|
27
|
+
name: str,
|
|
28
|
+
*,
|
|
29
|
+
given: Sequence[str], # state predicate names: p : state -> bool
|
|
30
|
+
when: Sequence[str], # action predicate names: q : state -> action -> bool (state first!)
|
|
31
|
+
state_type: str = "state",
|
|
32
|
+
action_type: str = "action",
|
|
33
|
+
state_var: str = "s",
|
|
34
|
+
action_var: str = "a",
|
|
35
|
+
) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Consistency query: only predicates (given/when), no transitions.
|
|
38
|
+
|
|
39
|
+
Produces:
|
|
40
|
+
(p1 s) && (q1 s a) && ...
|
|
41
|
+
"""
|
|
42
|
+
lines : list[str] = ["fun (s : state) (a : action) -> "]
|
|
43
|
+
|
|
44
|
+
exprs: list[str] = []
|
|
45
|
+
exprs += [f"{p} {state_var}" for p in given]
|
|
46
|
+
exprs += [f"{q} {state_var} {action_var}" for q in when]
|
|
47
|
+
|
|
48
|
+
lines.append(f" {and_all(exprs)}")
|
|
49
|
+
result = "\n".join(lines)
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def mk_same_action_trace_instance_src(
|
|
55
|
+
name: str,
|
|
56
|
+
*,
|
|
57
|
+
given: Sequence[str], # state predicate names
|
|
58
|
+
when: Sequence[str], # action predicate names
|
|
59
|
+
transitions: Sequence[str], # transition fn names: t : state -> action -> state
|
|
60
|
+
state_type: str = "state",
|
|
61
|
+
action_type: str = "action",
|
|
62
|
+
state0_var: str = "s0",
|
|
63
|
+
action_var: str = "a",
|
|
64
|
+
when_each_step: bool = True, # apply action predicates at each pre-state
|
|
65
|
+
) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Trace query: transitions applied sequentially using the SAME action `a`.
|
|
68
|
+
|
|
69
|
+
Produces:
|
|
70
|
+
|
|
71
|
+
instance <name> :
|
|
72
|
+
let s0 : state = s0 in
|
|
73
|
+
let a : action = a in
|
|
74
|
+
let s1 = t0 s0 a in
|
|
75
|
+
let s2 = t1 s1 a in
|
|
76
|
+
(given(s0)) && (when(s0,a)) && (when(s1,a)) ...
|
|
77
|
+
|
|
78
|
+
Note: for N transitions, `when` is applied to s0..s{N-1} if when_each_step=True.
|
|
79
|
+
"""
|
|
80
|
+
lines: list[str] = [f"instance {name} :"]
|
|
81
|
+
|
|
82
|
+
# typed existentials
|
|
83
|
+
lines.append(f" let {state0_var} : {state_type} = {state0_var} in")
|
|
84
|
+
lines.append(f" let {action_var} : {action_type} = {action_var} in")
|
|
85
|
+
|
|
86
|
+
# let-chain states
|
|
87
|
+
# s0 already exists; create s1..sN
|
|
88
|
+
for i, tname in enumerate(transitions):
|
|
89
|
+
si = state0_var if i == 0 else f"s{i}"
|
|
90
|
+
si1 = f"s{i+1}"
|
|
91
|
+
lines.append(f" let {si1} = {tname} {si} {action_var} in")
|
|
92
|
+
|
|
93
|
+
exprs: list[str] = []
|
|
94
|
+
exprs += [f"{p} {state0_var}" for p in given]
|
|
95
|
+
|
|
96
|
+
if when_each_step:
|
|
97
|
+
for i in range(len(transitions)):
|
|
98
|
+
si = state0_var if i == 0 else f"s{i}"
|
|
99
|
+
exprs += [f"{q} {si} {action_var}" for q in when]
|
|
100
|
+
else:
|
|
101
|
+
exprs += [f"{q} {state0_var} {action_var}" for q in when]
|
|
102
|
+
|
|
103
|
+
lines.append(f" {and_all(exprs)}")
|
|
104
|
+
return "\n".join(lines)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def mk_scenario_expr(sc: Scenario, *, s_var: str = "s", a_var: str = "a") -> str:
|
|
108
|
+
parts: list[str] = []
|
|
109
|
+
parts += [f"({p} {s_var})" for p in sc.given]
|
|
110
|
+
parts += [f"({q} {s_var} {a_var})" for q in sc.when]
|
|
111
|
+
return "true" if not parts else " && ".join(parts)
|
|
112
|
+
|
|
113
|
+
def mk_spec_disjunction_expr(scenarios: Sequence[Scenario]) -> str:
|
|
114
|
+
disj = " || ".join(f"({mk_scenario_expr(sc)})" for sc in scenarios)
|
|
115
|
+
return "false" if not disj else disj
|
|
116
|
+
|
|
117
|
+
def mk_goal_def(goal_name: str, goal_expr: str, *, needs_action: bool) -> str:
|
|
118
|
+
if needs_action:
|
|
119
|
+
return (
|
|
120
|
+
f"let {goal_name} (s : state) (a : action) : bool =\n"
|
|
121
|
+
f" {goal_expr}\n"
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
return (
|
|
125
|
+
f"let {goal_name} (s : state) : bool =\n"
|
|
126
|
+
f" {goal_expr}\n"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def collect_pred_names(scenarios: Sequence[Scenario]) -> tuple[list[str], list[str]]:
|
|
130
|
+
state_names: list[str] = []
|
|
131
|
+
action_names: list[str] = []
|
|
132
|
+
for sc in scenarios:
|
|
133
|
+
state_names += list(sc.given)
|
|
134
|
+
action_names += list(sc.when)
|
|
135
|
+
return state_names, action_names
|
|
136
|
+
|
|
137
|
+
LET_RE = re.compile(r"^\s*let\s+([a-zA-Z_][a-zA-Z0-9_']*)\s*(?::\s*[^=]+)?=\s*(.+)$", re.DOTALL)
|
|
138
|
+
|
|
139
|
+
def normalize_optional(s: str | None) -> str | None:
|
|
140
|
+
if s is None:
|
|
141
|
+
return None
|
|
142
|
+
s2 = s.strip()
|
|
143
|
+
return s2 if s2 else None
|
|
144
|
+
|
|
145
|
+
def mk_typed_let(binding_src: str, *, expected_type: str, default_name: str) -> tuple[str, str]:
|
|
146
|
+
"""
|
|
147
|
+
Returns: (let_src, bound_name)
|
|
148
|
+
Accepts either:
|
|
149
|
+
- `let x = ...` (or `let x : t = ...`) -> keeps x
|
|
150
|
+
- `<expr>` -> wraps as `let <default_name> : expected_type = (<expr>)`
|
|
151
|
+
"""
|
|
152
|
+
m = LET_RE.match(binding_src.strip())
|
|
153
|
+
if m:
|
|
154
|
+
name = m.group(1)
|
|
155
|
+
# Keep user's let as-is; we just trust their annotation (or lack of).
|
|
156
|
+
# (Type errors will be caught by eval_model.)
|
|
157
|
+
return binding_src.strip(), name
|
|
158
|
+
|
|
159
|
+
name = default_name
|
|
160
|
+
let_src = f"let {name} : {expected_type} = (\n{binding_src.strip()}\n)"
|
|
161
|
+
return let_src, name
|
|
162
|
+
|
|
163
|
+
def mk_instance_src_for_expr(*, name: str, expr: str, needs_action: bool) -> str:
|
|
164
|
+
# Assumes `state` and (optionally) `action` exist in the model context.
|
|
165
|
+
# `expr` must be boolean and can mention `s` and optionally `a`.
|
|
166
|
+
if needs_action:
|
|
167
|
+
return f"""
|
|
168
|
+
instance {name} =
|
|
169
|
+
{{ s : state; a : action }}
|
|
170
|
+
where {expr}
|
|
171
|
+
"""
|
|
172
|
+
return f"""
|
|
173
|
+
instance {name} =
|
|
174
|
+
{{ s : state }}
|
|
175
|
+
where {expr}
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def errs_to_string(eval_res: object) -> str | None:
|
|
179
|
+
"""
|
|
180
|
+
Your eval_model result looks like it may have .has_errors and .errors.
|
|
181
|
+
Convert to a multi-line string.
|
|
182
|
+
"""
|
|
183
|
+
if not bool(getattr(eval_res, "has_errors", False)):
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
errs = getattr(eval_res, "errors", None)
|
|
187
|
+
if not errs:
|
|
188
|
+
return "Domain/trace model has errors"
|
|
189
|
+
|
|
190
|
+
# errs items could be strings or structured; stringify safely
|
|
191
|
+
lines: list[str] = []
|
|
192
|
+
for e in errs:
|
|
193
|
+
lines.append(str(e))
|
|
194
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Imandra Inc.
|
|
3
|
+
#
|
|
4
|
+
# speclogician/logic/utils.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
InstanceStatus = Literal["sat", "unsat", "unknown", "err"]
|
|
12
|
+
|
|
13
|
+
_ONEOF_CANDIDATES = ("result", "res", "outcome", "status", "response")
|
|
14
|
+
|
|
15
|
+
def instance_status(res: Any) -> InstanceStatus:
|
|
16
|
+
|
|
17
|
+
# 1) Prefer oneof introspection if available
|
|
18
|
+
for oneof_name in _ONEOF_CANDIDATES:
|
|
19
|
+
try:
|
|
20
|
+
which = res.WhichOneof(oneof_name)
|
|
21
|
+
except Exception:
|
|
22
|
+
continue
|
|
23
|
+
if which in ("sat", "unsat", "unknown", "err"):
|
|
24
|
+
return which # type: ignore[return-value]
|
|
25
|
+
|
|
26
|
+
# 2) Fallback to HasField checks
|
|
27
|
+
for k in ("sat", "unsat", "unknown", "err"):
|
|
28
|
+
try:
|
|
29
|
+
if res.HasField(k):
|
|
30
|
+
return k # type: ignore[return-value]
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
# 3) Conservative fallback
|
|
35
|
+
return "unknown"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def instance_is_sat(res: Any) -> bool:
|
|
39
|
+
return res.sat is not None
|
|
40
|
+
|
|
41
|
+
def instance_model(res: Any) -> Any | None:
|
|
42
|
+
"""
|
|
43
|
+
Returns Model if SAT, else None.
|
|
44
|
+
"""
|
|
45
|
+
return res.sat.model.src if res.sat is not None and res.sat.model is not None else None
|
|
46
|
+
|
|
47
|
+
def instance_model_src(res: Any) -> str | None:
|
|
48
|
+
"""
|
|
49
|
+
Returns Model.src if SAT, else None.
|
|
50
|
+
"""
|
|
51
|
+
m = instance_model(res)
|
|
52
|
+
if m is None:
|
|
53
|
+
return None
|
|
54
|
+
return getattr(m, "src", None)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def instance_model_artifact(res: Any) -> Any | None:
|
|
58
|
+
"""
|
|
59
|
+
Returns Model.artifact if SAT, else None.
|
|
60
|
+
"""
|
|
61
|
+
m = instance_model(res)
|
|
62
|
+
if m is None:
|
|
63
|
+
return None
|
|
64
|
+
return getattr(m, "artifact", None)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def instance_unsat_proof(res: Any) -> str | None:
|
|
68
|
+
"""
|
|
69
|
+
Returns Unsat.proof_pp if UNSAT, else None.
|
|
70
|
+
"""
|
|
71
|
+
if instance_status(res) != "unsat":
|
|
72
|
+
return None
|
|
73
|
+
try:
|
|
74
|
+
return res.unsat.proof_pp
|
|
75
|
+
except Exception:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def instance_unknown_reason(res: Any) -> str | None:
|
|
80
|
+
"""
|
|
81
|
+
Returns unknown StringMsg content if available.
|
|
82
|
+
`_utils_pb2.StringMsg` typically stores a `msg` or `value` field; handle both.
|
|
83
|
+
"""
|
|
84
|
+
if instance_status(res) != "unknown":
|
|
85
|
+
return None
|
|
86
|
+
u = getattr(res, "unknown", None)
|
|
87
|
+
if u is None:
|
|
88
|
+
return None
|
|
89
|
+
return getattr(u, "msg", None) or getattr(u, "value", None) or str(u)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def instance_has_errors(res: Any) -> bool:
|
|
93
|
+
try:
|
|
94
|
+
return len(res.errors) > 0
|
|
95
|
+
except Exception:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def instance_to_jsonable(res: Any) -> dict[str, Any]:
|
|
100
|
+
"""
|
|
101
|
+
Minimal, stable JSONable view for SpecLogician logs/CLI:
|
|
102
|
+
- status
|
|
103
|
+
- model.src (if sat)
|
|
104
|
+
- unsat.proof_pp (if unsat)
|
|
105
|
+
- unknown reason (if unknown)
|
|
106
|
+
- errors count (+ optionally stringified errors)
|
|
107
|
+
- task (stringified)
|
|
108
|
+
"""
|
|
109
|
+
st = instance_status(res)
|
|
110
|
+
out: dict[str, Any] = {
|
|
111
|
+
"status": st,
|
|
112
|
+
"has_errors": instance_has_errors(res),
|
|
113
|
+
"errors_count": len(res.errors) if hasattr(res, "errors") else 0,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if st == "sat":
|
|
117
|
+
m = res.sat.model
|
|
118
|
+
out["model"] = {
|
|
119
|
+
"m_type": str(getattr(m, "m_type", "")),
|
|
120
|
+
"src": getattr(m, "src", ""),
|
|
121
|
+
# artifact can be huge; include a string repr unless you want deeper extraction
|
|
122
|
+
"artifact": str(getattr(m, "artifact", "")),
|
|
123
|
+
}
|
|
124
|
+
elif st == "unsat":
|
|
125
|
+
out["proof_pp"] = getattr(res.unsat, "proof_pp", "")
|
|
126
|
+
elif st == "unknown":
|
|
127
|
+
out["reason"] = instance_unknown_reason(res)
|
|
128
|
+
elif st == "err":
|
|
129
|
+
out["reason"] = "err"
|
|
130
|
+
|
|
131
|
+
# Task is protobuf; stringify safely
|
|
132
|
+
if hasattr(res, "task"):
|
|
133
|
+
out["task"] = str(res.task)
|
|
134
|
+
|
|
135
|
+
return out
|