semanticdltl 1.1.0__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.
- dltl/__init__.py +39 -0
- dltl/__main__.py +25 -0
- dltl/cli.py +181 -0
- dltl/evaluator.py +393 -0
- dltl/formula.py +170 -0
- dltl/log.py +271 -0
- dltl/macros.py +64 -0
- dltl/parser.py +229 -0
- dltl/propositions.py +103 -0
- dltl/session.py +265 -0
- semanticdltl-1.1.0.dist-info/METADATA +591 -0
- semanticdltl-1.1.0.dist-info/RECORD +15 -0
- semanticdltl-1.1.0.dist-info/WHEEL +4 -0
- semanticdltl-1.1.0.dist-info/entry_points.txt +2 -0
- semanticdltl-1.1.0.dist-info/licenses/LICENSE +674 -0
dltl/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# SemanticDLTL: a DLTL model checker over finite traces
|
|
3
|
+
#
|
|
4
|
+
# Copyright (C) 2024-2026
|
|
5
|
+
# Joaquín Ezpeleta, Universidad de Zaragoza, Spain
|
|
6
|
+
# Javier Fabra, Universidad de Zaragoza, Spain
|
|
7
|
+
# María José Ibáñez, Universidad de La Rioja, Spain
|
|
8
|
+
# Contact: semanticdltl@unizar.es
|
|
9
|
+
#
|
|
10
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
11
|
+
#
|
|
12
|
+
# This file is part of SemanticDLTL. It is free software: you can redistribute
|
|
13
|
+
# it and/or modify it under the terms of the GNU General Public License as
|
|
14
|
+
# published by the Free Software Foundation, either version 3 of the License,
|
|
15
|
+
# or (at your option) any later version. See the LICENSE file for details.
|
|
16
|
+
#
|
|
17
|
+
# Based on: J. M. Couvreur, J. Ezpeleta, "A Linear Temporal Logic Model
|
|
18
|
+
# Checking Method over Finite Words with Correlated Transition Attributes",
|
|
19
|
+
# SIMPDA 2017, LNBIP vol. 340, Springer, 2019.
|
|
20
|
+
# -----------------------------------------------------------------------------
|
|
21
|
+
"""SemanticDLTL: a model checker for DLTL (data-aware LTL with freeze operators) over finite traces.
|
|
22
|
+
|
|
23
|
+
Typical use as a library::
|
|
24
|
+
|
|
25
|
+
from dltl import Log, Session
|
|
26
|
+
|
|
27
|
+
session = Session(Log.load("examples/sample"))
|
|
28
|
+
session.check_formula('F x.(b & "(x)x[V] == 10")')
|
|
29
|
+
session.execute("_WRITE")
|
|
30
|
+
"""
|
|
31
|
+
__version__ = "1.1.0"
|
|
32
|
+
|
|
33
|
+
from dltl.evaluator import Evaluator, results_statistics # noqa: E402
|
|
34
|
+
from dltl.log import I_ATOM, I_POS, Log # noqa: E402
|
|
35
|
+
from dltl.parser import parse_formula # noqa: E402
|
|
36
|
+
from dltl.session import CheckSummary, Session # noqa: E402
|
|
37
|
+
|
|
38
|
+
__all__ = ["I_ATOM", "I_POS", "CheckSummary", "Evaluator", "Log", "Session", "parse_formula",
|
|
39
|
+
"results_statistics", "__version__"]
|
dltl/__main__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# SemanticDLTL: a DLTL model checker over finite traces
|
|
3
|
+
#
|
|
4
|
+
# Copyright (C) 2024-2026
|
|
5
|
+
# Joaquín Ezpeleta, Universidad de Zaragoza, Spain
|
|
6
|
+
# Javier Fabra, Universidad de Zaragoza, Spain
|
|
7
|
+
# María José Ibáñez, Universidad de La Rioja, Spain
|
|
8
|
+
# Contact: semanticdltl@unizar.es
|
|
9
|
+
#
|
|
10
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
11
|
+
#
|
|
12
|
+
# This file is part of SemanticDLTL. It is free software: you can redistribute
|
|
13
|
+
# it and/or modify it under the terms of the GNU General Public License as
|
|
14
|
+
# published by the Free Software Foundation, either version 3 of the License,
|
|
15
|
+
# or (at your option) any later version. See the LICENSE file for details.
|
|
16
|
+
#
|
|
17
|
+
# Based on: J. M. Couvreur, J. Ezpeleta, "A Linear Temporal Logic Model
|
|
18
|
+
# Checking Method over Finite Words with Correlated Transition Attributes",
|
|
19
|
+
# SIMPDA 2017, LNBIP vol. 340, Springer, 2019.
|
|
20
|
+
# -----------------------------------------------------------------------------
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from dltl.cli import main
|
|
24
|
+
|
|
25
|
+
sys.exit(main())
|
dltl/cli.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# SemanticDLTL: a DLTL model checker over finite traces
|
|
3
|
+
#
|
|
4
|
+
# Copyright (C) 2024-2026
|
|
5
|
+
# Joaquín Ezpeleta, Universidad de Zaragoza, Spain
|
|
6
|
+
# Javier Fabra, Universidad de Zaragoza, Spain
|
|
7
|
+
# María José Ibáñez, Universidad de La Rioja, Spain
|
|
8
|
+
# Contact: semanticdltl@unizar.es
|
|
9
|
+
#
|
|
10
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
11
|
+
#
|
|
12
|
+
# This file is part of SemanticDLTL. It is free software: you can redistribute
|
|
13
|
+
# it and/or modify it under the terms of the GNU General Public License as
|
|
14
|
+
# published by the Free Software Foundation, either version 3 of the License,
|
|
15
|
+
# or (at your option) any later version. See the LICENSE file for details.
|
|
16
|
+
#
|
|
17
|
+
# Based on: J. M. Couvreur, J. Ezpeleta, "A Linear Temporal Logic Model
|
|
18
|
+
# Checking Method over Finite Words with Correlated Transition Attributes",
|
|
19
|
+
# SIMPDA 2017, LNBIP vol. 340, Springer, 2019.
|
|
20
|
+
# -----------------------------------------------------------------------------
|
|
21
|
+
"""
|
|
22
|
+
Command-line interface of the model checker.
|
|
23
|
+
|
|
24
|
+
dltl-mc --log-file <model> [--init-file F] [--formula-file F]
|
|
25
|
+
[--no-interactive] [--multi-line] [--propositions F.py]
|
|
26
|
+
|
|
27
|
+
The legacy ``key=value`` syntax is also accepted:
|
|
28
|
+
|
|
29
|
+
python MC.py log-file=<model> init-file=F formula-file=F interactive=false
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import importlib.util
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
import time
|
|
38
|
+
from collections.abc import Iterator
|
|
39
|
+
from types import ModuleType
|
|
40
|
+
from typing import TextIO
|
|
41
|
+
|
|
42
|
+
from dltl import __version__
|
|
43
|
+
from dltl.log import Log
|
|
44
|
+
from dltl.session import Session
|
|
45
|
+
|
|
46
|
+
PROMPT = "DLTL -> "
|
|
47
|
+
MULTI_LINE_END = '$'
|
|
48
|
+
|
|
49
|
+
_LEGACY_ARG = re.compile(
|
|
50
|
+
r'(log-file|init-file|formula-file|interactive|multi-line|propositions)=(.*)')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalize_legacy_argv(argv: list[str]) -> list[str]:
|
|
54
|
+
"""Translate ``key=value`` arguments into their ``--key value`` form."""
|
|
55
|
+
out: list[str] = []
|
|
56
|
+
for arg in argv:
|
|
57
|
+
m = _LEGACY_ARG.fullmatch(arg)
|
|
58
|
+
if not m:
|
|
59
|
+
out.append(arg)
|
|
60
|
+
continue
|
|
61
|
+
key, value = m.groups()
|
|
62
|
+
if key == 'interactive':
|
|
63
|
+
out.append('--no-interactive' if value.strip().lower() == 'false' else '--interactive')
|
|
64
|
+
elif key == 'multi-line':
|
|
65
|
+
if value.strip().lower() == 'true':
|
|
66
|
+
out.append('--multi-line')
|
|
67
|
+
else:
|
|
68
|
+
out += [f'--{key}', value]
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
73
|
+
p = argparse.ArgumentParser(
|
|
74
|
+
prog="dltl-mc",
|
|
75
|
+
description="Model checker for DLTL formulas over the traces of a .mod file.",
|
|
76
|
+
epilog="The legacy syntax key=value (e.g. log-file=model interactive=false) "
|
|
77
|
+
"is also accepted.")
|
|
78
|
+
p.add_argument('--log-file', required=True, metavar='MODEL',
|
|
79
|
+
help="trace model, with or without the .mod suffix")
|
|
80
|
+
p.add_argument('--init-file', metavar='FILE',
|
|
81
|
+
help="file with commands executed before reading formulas")
|
|
82
|
+
p.add_argument('--formula-file', metavar='FILE',
|
|
83
|
+
help="file with the formulas to check (default: standard input)")
|
|
84
|
+
p.add_argument('--propositions', metavar='FILE.py',
|
|
85
|
+
help="Python file with the user propositions available as PROP "
|
|
86
|
+
"(default: dltl.propositions)")
|
|
87
|
+
p.add_argument('--interactive', action=argparse.BooleanOptionalAction, default=True,
|
|
88
|
+
help="show a prompt when reading formulas from standard input")
|
|
89
|
+
p.add_argument('--multi-line', action='store_true',
|
|
90
|
+
help=f"formulas may span several lines and end with '{MULTI_LINE_END}'")
|
|
91
|
+
p.add_argument('--version', action='version', version=f"%(prog)s {__version__}")
|
|
92
|
+
return p
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# input readers
|
|
97
|
+
|
|
98
|
+
def read_lines(stream: TextIO, prompt: str = "", multi_line: bool = False) -> Iterator[str]:
|
|
99
|
+
"""Yield formulas/commands from ``stream``, one per line or, in multi-line
|
|
100
|
+
mode, joining lines until one containing ``$``."""
|
|
101
|
+
buffer: list[str] = []
|
|
102
|
+
while True:
|
|
103
|
+
if prompt:
|
|
104
|
+
print(prompt, end='', flush=True)
|
|
105
|
+
line = stream.readline()
|
|
106
|
+
if line == '': # EOF
|
|
107
|
+
if buffer:
|
|
108
|
+
yield ''.join(buffer)
|
|
109
|
+
if prompt:
|
|
110
|
+
print("EOF found when reading formula", file=sys.stderr)
|
|
111
|
+
return
|
|
112
|
+
line = line.rstrip('\n')
|
|
113
|
+
if not multi_line:
|
|
114
|
+
yield line
|
|
115
|
+
elif MULTI_LINE_END in line:
|
|
116
|
+
buffer.append(line.split(MULTI_LINE_END)[0])
|
|
117
|
+
yield ''.join(buffer)
|
|
118
|
+
buffer = []
|
|
119
|
+
else:
|
|
120
|
+
buffer.append(line)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def read_file(path: str, multi_line: bool = False) -> Iterator[str]:
|
|
124
|
+
with open(path) as f:
|
|
125
|
+
yield from read_lines(f, multi_line=multi_line)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def load_propositions(path: str | None) -> ModuleType:
|
|
129
|
+
if path is None:
|
|
130
|
+
from dltl import propositions
|
|
131
|
+
return propositions
|
|
132
|
+
spec = importlib.util.spec_from_file_location("user_propositions", path)
|
|
133
|
+
if spec is None or spec.loader is None:
|
|
134
|
+
raise ImportError(f"cannot load propositions from '{path}'")
|
|
135
|
+
module = importlib.util.module_from_spec(spec)
|
|
136
|
+
spec.loader.exec_module(module)
|
|
137
|
+
return module
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def main(argv: list[str] | None = None) -> int:
|
|
143
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
144
|
+
parser = build_parser()
|
|
145
|
+
if not argv:
|
|
146
|
+
parser.print_help(sys.stderr)
|
|
147
|
+
return 2
|
|
148
|
+
args = parser.parse_args(normalize_legacy_argv(argv))
|
|
149
|
+
|
|
150
|
+
for path in (args.init_file, args.formula_file, args.propositions):
|
|
151
|
+
if path is not None:
|
|
152
|
+
try:
|
|
153
|
+
open(path).close()
|
|
154
|
+
except OSError as e:
|
|
155
|
+
print(f"Error: cannot read '{path}': {e.strerror}", file=sys.stderr)
|
|
156
|
+
return 1
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
props = load_propositions(args.propositions)
|
|
160
|
+
start = time.time()
|
|
161
|
+
log = Log.load(args.log_file)
|
|
162
|
+
loading_time = time.time() - start
|
|
163
|
+
except (OSError, ValueError, ImportError) as e:
|
|
164
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
165
|
+
return 1
|
|
166
|
+
|
|
167
|
+
session = Session(log, props)
|
|
168
|
+
if args.interactive:
|
|
169
|
+
print(f"Loading time: {loading_time:.4f} seconds")
|
|
170
|
+
print(log.info())
|
|
171
|
+
|
|
172
|
+
if args.init_file and not session.run(read_file(args.init_file)):
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
if args.formula_file:
|
|
176
|
+
lines = read_file(args.formula_file, multi_line=args.multi_line)
|
|
177
|
+
else:
|
|
178
|
+
prompt = PROMPT if args.interactive else ""
|
|
179
|
+
lines = read_lines(sys.stdin, prompt=prompt, multi_line=args.multi_line)
|
|
180
|
+
session.run(lines)
|
|
181
|
+
return 0
|
dltl/evaluator.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# SemanticDLTL: a DLTL model checker over finite traces
|
|
3
|
+
#
|
|
4
|
+
# Copyright (C) 2024-2026
|
|
5
|
+
# Joaquín Ezpeleta, Universidad de Zaragoza, Spain
|
|
6
|
+
# Javier Fabra, Universidad de Zaragoza, Spain
|
|
7
|
+
# María José Ibáñez, Universidad de La Rioja, Spain
|
|
8
|
+
# Contact: semanticdltl@unizar.es
|
|
9
|
+
#
|
|
10
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
11
|
+
#
|
|
12
|
+
# This file is part of SemanticDLTL. It is free software: you can redistribute
|
|
13
|
+
# it and/or modify it under the terms of the GNU General Public License as
|
|
14
|
+
# published by the Free Software Foundation, either version 3 of the License,
|
|
15
|
+
# or (at your option) any later version. See the LICENSE file for details.
|
|
16
|
+
#
|
|
17
|
+
# Based on: J. M. Couvreur, J. Ezpeleta, "A Linear Temporal Logic Model
|
|
18
|
+
# Checking Method over Finite Words with Correlated Transition Attributes",
|
|
19
|
+
# SIMPDA 2017, LNBIP vol. 340, Springer, 2019.
|
|
20
|
+
# -----------------------------------------------------------------------------
|
|
21
|
+
"""
|
|
22
|
+
Evaluation of DLTL formulas over a finite trace.
|
|
23
|
+
|
|
24
|
+
The evaluation is bottom-up and per trace: ``Evaluator.eval_formula`` returns a
|
|
25
|
+
list with one (partially evaluated) node per event, the node at position ``i``
|
|
26
|
+
being the value of the formula at event ``i``. When a formula contains no free
|
|
27
|
+
freeze variables that node is simply ``TRUE()`` or ``FALSE()``.
|
|
28
|
+
|
|
29
|
+
Model checker based on the DLTL logic and algorithm in:
|
|
30
|
+
J. M. Couvreur, J. Ezpeleta, "A Linear Temporal Logic Model Checking
|
|
31
|
+
Method over Finite Words with Correlated Transition Attributes",
|
|
32
|
+
SIMPDA 2017, LNBIP vol. 340, Springer, 2019.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import re
|
|
37
|
+
import sys
|
|
38
|
+
import traceback
|
|
39
|
+
from types import ModuleType
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
from dltl.formula import AND, FALSE, NOT, OR, TRUE, e1, e2, is_false, is_true, t, v
|
|
43
|
+
from dltl.log import I_ATOM, I_POS
|
|
44
|
+
|
|
45
|
+
# Name under which the current trace is visible inside data expressions after
|
|
46
|
+
# the freeze variables have been substituted ("x[V]" -> "THE_TRACE[3][V]").
|
|
47
|
+
TRACE_NAME = 'THE_TRACE'
|
|
48
|
+
|
|
49
|
+
_UNARY_OPS = frozenset({'atom', '!', 'X', 'G', 'F', 'Y', 'H', 'O'})
|
|
50
|
+
_BINARY_OPS = frozenset({'&', '|', 'U', 'S'})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Evaluator:
|
|
54
|
+
"""Evaluates formulas over traces of a given model.
|
|
55
|
+
|
|
56
|
+
``column_index`` maps attribute names to positions in the event tuple (see
|
|
57
|
+
:class:`dltl.log.Log`), and ``props`` is the module exposed as ``PROP``
|
|
58
|
+
inside data expressions. Both, together with the current trace, form the
|
|
59
|
+
namespace in which data expressions are ``eval``'d.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, column_index: dict[str, int], props: ModuleType | None = None):
|
|
63
|
+
if props is None:
|
|
64
|
+
from dltl import propositions as props
|
|
65
|
+
self._ns: dict[str, Any] = {**column_index,
|
|
66
|
+
'COL': dict(column_index),
|
|
67
|
+
'I_POS': I_POS,
|
|
68
|
+
'I_ATOM': I_ATOM,
|
|
69
|
+
'PROP': props,
|
|
70
|
+
TRACE_NAME: ()}
|
|
71
|
+
self._cases = {
|
|
72
|
+
'X': self.eval_X, 'U': self.eval_U, 'F': self.eval_F, 'G': self.eval_G,
|
|
73
|
+
'Y': self.eval_Y, 'S': self.eval_S, 'O': self.eval_O, 'H': self.eval_H,
|
|
74
|
+
'True': self.eval_True_False, 'False': self.eval_True_False,
|
|
75
|
+
'!': self.eval_Not, '&': self.eval_AND, '|': self.eval_OR,
|
|
76
|
+
'fvar': self.eval_fvar, 'exp': self.eval_exp, 'atom': self.eval_atom,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def add_module(self, name: str, module: ModuleType) -> None:
|
|
80
|
+
"""Make ``module`` available as ``name`` inside data expressions (``_LOAD``)."""
|
|
81
|
+
self._ns[name] = module
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
def eval_formula(self, exp, trace) -> list:
|
|
85
|
+
"""Evaluate ``exp`` at every event of ``trace``; one node per event."""
|
|
86
|
+
self._ns[TRACE_NAME] = trace
|
|
87
|
+
try:
|
|
88
|
+
return self._cases[exp[t]](exp, trace)
|
|
89
|
+
except Exception as e: # noqa: BLE001 - report and keep the session alive
|
|
90
|
+
print(f"An error occurred: {e}", file=sys.stderr)
|
|
91
|
+
print("\n--- Full Traceback ---", file=sys.stderr)
|
|
92
|
+
traceback.print_exc(file=sys.stderr)
|
|
93
|
+
print("returning FALSE value\n", file=sys.stderr)
|
|
94
|
+
print("----------------------\n", file=sys.stderr)
|
|
95
|
+
return [FALSE() for _ in trace]
|
|
96
|
+
|
|
97
|
+
def _eval_data(self, expression: str):
|
|
98
|
+
return eval(expression, self._ns) # noqa: S307 - data expressions are user code by design
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
def replace(self, traza, i, exp, var):
|
|
102
|
+
"""Substitute freeze variable ``var`` by event ``i`` in ``exp``.
|
|
103
|
+
|
|
104
|
+
``var[#]`` becomes the (1-based) position of the event and any other
|
|
105
|
+
occurrence of ``var`` becomes ``THE_TRACE[i]``. Data expressions left
|
|
106
|
+
without free variables are evaluated on the spot.
|
|
107
|
+
"""
|
|
108
|
+
# everything that depends only on (i, var) is computed once per call
|
|
109
|
+
var_set = frozenset({var})
|
|
110
|
+
position = str(i + 1) # first event position is 1, not 0
|
|
111
|
+
trace_ref = f"{TRACE_NAME}[{i}]"
|
|
112
|
+
var_hash = f"{var}[#]"
|
|
113
|
+
pattern = re.compile(rf'\b{re.escape(var)}\b')
|
|
114
|
+
|
|
115
|
+
stack = [(exp, False)]
|
|
116
|
+
resultMap = {}
|
|
117
|
+
|
|
118
|
+
while stack:
|
|
119
|
+
theForm, visited = stack.pop()
|
|
120
|
+
form_id = id(theForm)
|
|
121
|
+
if form_id in resultMap:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
vars = theForm[v]
|
|
125
|
+
op = theForm[t]
|
|
126
|
+
|
|
127
|
+
if var not in vars or is_false(theForm) or is_true(theForm):
|
|
128
|
+
resultMap[form_id] = theForm
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if visited: # descendant results are already in resultMap
|
|
132
|
+
new_vars = vars - var_set
|
|
133
|
+
if op in _UNARY_OPS:
|
|
134
|
+
resultMap[form_id] = [new_vars, op, resultMap[id(theForm[e1])]]
|
|
135
|
+
elif op in _BINARY_OPS:
|
|
136
|
+
resultMap[form_id] = [new_vars, op,
|
|
137
|
+
resultMap[id(theForm[e1])],
|
|
138
|
+
resultMap[id(theForm[e2])]]
|
|
139
|
+
elif op == 'fvar':
|
|
140
|
+
resultMap[form_id] = [new_vars, op, theForm[e1], resultMap[id(theForm[e2])]]
|
|
141
|
+
elif op == 'exp':
|
|
142
|
+
newFormula = theForm[e1].replace(var_hash, position)
|
|
143
|
+
newFormula = pattern.sub(trace_ref, newFormula)
|
|
144
|
+
if not new_vars:
|
|
145
|
+
resultMap[form_id] = [new_vars, str(self._eval_data(newFormula))]
|
|
146
|
+
else:
|
|
147
|
+
resultMap[form_id] = [new_vars, op, newFormula]
|
|
148
|
+
else:
|
|
149
|
+
# postorder: reinsert with visited=True and process descendants
|
|
150
|
+
stack.append((theForm, True))
|
|
151
|
+
if op in _UNARY_OPS:
|
|
152
|
+
stack.append((theForm[e1], False))
|
|
153
|
+
elif op in _BINARY_OPS:
|
|
154
|
+
stack.append((theForm[e1], False))
|
|
155
|
+
stack.append((theForm[e2], False))
|
|
156
|
+
elif op == 'fvar':
|
|
157
|
+
stack.append((theForm[e2], False))
|
|
158
|
+
|
|
159
|
+
return resultMap[id(exp)]
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
def eval_formula_in_event(self, exp, i, traza):
|
|
163
|
+
"""Simplify ``exp`` at event ``i`` as far as its free variables allow."""
|
|
164
|
+
# (exp, False): children not yet processed
|
|
165
|
+
# (exp, True): children already resolved and their value stored in resultMap
|
|
166
|
+
stack = [(exp, False)]
|
|
167
|
+
resultMap = {}
|
|
168
|
+
|
|
169
|
+
while stack:
|
|
170
|
+
theForm, visited = stack.pop()
|
|
171
|
+
|
|
172
|
+
if id(theForm) in resultMap:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
vars = theForm[v]
|
|
176
|
+
op = theForm[t]
|
|
177
|
+
|
|
178
|
+
# true or false, or non-evaluable because of the vars
|
|
179
|
+
if len(vars) != 0 or is_false(theForm) or is_true(theForm):
|
|
180
|
+
resultMap[id(theForm)] = theForm
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
if visited:
|
|
184
|
+
if op == 'exp':
|
|
185
|
+
val = theForm[e1]
|
|
186
|
+
try:
|
|
187
|
+
val_str = str(self._eval_data(str(val)))
|
|
188
|
+
except Exception as ex: # noqa: BLE001
|
|
189
|
+
print(f"Eval error in 'exp': {ex}", file=sys.stderr)
|
|
190
|
+
val_str = "False"
|
|
191
|
+
resultMap[id(theForm)] = [set(), val_str]
|
|
192
|
+
elif op == 'atom':
|
|
193
|
+
resultMap[id(theForm)] = TRUE() if theForm[e1] in traza[i][I_ATOM] else FALSE()
|
|
194
|
+
elif op == '&':
|
|
195
|
+
ev1 = resultMap[id(theForm[e1])]
|
|
196
|
+
ev2 = resultMap[id(theForm[e2])]
|
|
197
|
+
if is_false(ev1) or is_false(ev2):
|
|
198
|
+
resultMap[id(theForm)] = FALSE()
|
|
199
|
+
elif is_true(ev1) and is_true(ev2):
|
|
200
|
+
resultMap[id(theForm)] = TRUE()
|
|
201
|
+
elif is_true(ev1):
|
|
202
|
+
resultMap[id(theForm)] = ev2
|
|
203
|
+
elif is_true(ev2):
|
|
204
|
+
resultMap[id(theForm)] = ev1
|
|
205
|
+
else:
|
|
206
|
+
resultMap[id(theForm)] = [ev1[0] | ev2[0], '&', ev1, ev2]
|
|
207
|
+
elif op == '|':
|
|
208
|
+
ev1 = resultMap[id(theForm[e1])]
|
|
209
|
+
ev2 = resultMap[id(theForm[e2])]
|
|
210
|
+
if is_true(ev1) or is_true(ev2):
|
|
211
|
+
resultMap[id(theForm)] = TRUE()
|
|
212
|
+
elif is_false(ev1):
|
|
213
|
+
resultMap[id(theForm)] = ev2
|
|
214
|
+
elif is_false(ev2):
|
|
215
|
+
resultMap[id(theForm)] = ev1
|
|
216
|
+
else:
|
|
217
|
+
resultMap[id(theForm)] = [ev1[0] | ev2[0], '|', ev1, ev2]
|
|
218
|
+
elif op == '!':
|
|
219
|
+
ev = resultMap[id(theForm[e1])]
|
|
220
|
+
if len(ev[0]) == 0:
|
|
221
|
+
resultMap[id(theForm)] = FALSE() if ev[1] == 'True' else TRUE()
|
|
222
|
+
else:
|
|
223
|
+
resultMap[id(theForm)] = [ev[0], '!', ev]
|
|
224
|
+
elif op == 'fvar':
|
|
225
|
+
new_exp = self.replace(traza, i, theForm[e2], theForm[e1])
|
|
226
|
+
stack.append((new_exp, False))
|
|
227
|
+
stack.append((theForm, True)) # cannot be resolved yet
|
|
228
|
+
else:
|
|
229
|
+
stack.append((theForm, True))
|
|
230
|
+
# preorder: descendants first
|
|
231
|
+
if op in {'&', '|'}:
|
|
232
|
+
stack.append((theForm[e2], False))
|
|
233
|
+
stack.append((theForm[e1], False))
|
|
234
|
+
elif op == '!':
|
|
235
|
+
stack.append((theForm[e1], False))
|
|
236
|
+
# 'fvar' is handled in the post-processing step; 'atom'/'exp' have no children
|
|
237
|
+
|
|
238
|
+
return resultMap[id(exp)]
|
|
239
|
+
|
|
240
|
+
# ------------------------------------------------------------------
|
|
241
|
+
# future operators
|
|
242
|
+
def eval_X(self, exp, traza):
|
|
243
|
+
n = len(traza)
|
|
244
|
+
res = [None] * n
|
|
245
|
+
# "X false" is the idiom for "last event": it holds only there
|
|
246
|
+
res[n - 1] = TRUE() if is_false(exp[e1]) else FALSE()
|
|
247
|
+
eval_exp = self.eval_formula(exp[e1], traza)
|
|
248
|
+
for i in reversed(range(n - 1)):
|
|
249
|
+
res[i] = eval_exp[i + 1]
|
|
250
|
+
return res
|
|
251
|
+
|
|
252
|
+
def eval_U(self, exp, traza):
|
|
253
|
+
n = len(traza)
|
|
254
|
+
res = [None] * n
|
|
255
|
+
res_f = self.eval_formula(exp[e1], traza)
|
|
256
|
+
res_g = self.eval_formula(exp[e2], traza)
|
|
257
|
+
res[n - 1] = res_g[n - 1]
|
|
258
|
+
for i in reversed(range(n - 1)):
|
|
259
|
+
res[i] = self.eval_formula_in_event(
|
|
260
|
+
OR(res_g[i], AND(res_f[i], res[i + 1])), i, traza)
|
|
261
|
+
return res
|
|
262
|
+
|
|
263
|
+
def eval_F(self, exp, traza):
|
|
264
|
+
n = len(traza)
|
|
265
|
+
res = [None] * n
|
|
266
|
+
res_parcial = self.eval_formula(exp[e1], traza)
|
|
267
|
+
res[n - 1] = self.eval_formula_in_event(res_parcial[n - 1], n - 1, traza)
|
|
268
|
+
already_true = is_true(res[n - 1])
|
|
269
|
+
for i in reversed(range(n - 1)):
|
|
270
|
+
if already_true: # F f holds at i+1, hence at every earlier event
|
|
271
|
+
res[i] = TRUE()
|
|
272
|
+
else:
|
|
273
|
+
res[i] = self.eval_formula_in_event(OR(res_parcial[i], res[i + 1]), i, traza)
|
|
274
|
+
already_true = is_true(res[i])
|
|
275
|
+
return res
|
|
276
|
+
|
|
277
|
+
def eval_G(self, exp, traza):
|
|
278
|
+
n = len(traza)
|
|
279
|
+
res = [None] * n
|
|
280
|
+
res_parcial = self.eval_formula(exp[e1], traza)
|
|
281
|
+
res[n - 1] = self.eval_formula_in_event(res_parcial[n - 1], n - 1, traza)
|
|
282
|
+
already_false = is_false(res[n - 1])
|
|
283
|
+
for i in reversed(range(n - 1)):
|
|
284
|
+
if already_false: # G f fails at i+1, hence at every earlier event
|
|
285
|
+
res[i] = FALSE()
|
|
286
|
+
else:
|
|
287
|
+
res[i] = self.eval_formula_in_event(AND(res_parcial[i], res[i + 1]), i, traza)
|
|
288
|
+
already_false = is_false(res[i])
|
|
289
|
+
return res
|
|
290
|
+
|
|
291
|
+
# past operators
|
|
292
|
+
def eval_Y(self, exp, traza):
|
|
293
|
+
n = len(traza)
|
|
294
|
+
res = [None] * n
|
|
295
|
+
# "Y false" is the idiom for "first event": it holds only there
|
|
296
|
+
res[0] = TRUE() if is_false(exp[e1]) else FALSE()
|
|
297
|
+
res_exp = self.eval_formula(exp[e1], traza)
|
|
298
|
+
for i in range(1, n):
|
|
299
|
+
res[i] = res_exp[i - 1]
|
|
300
|
+
return res
|
|
301
|
+
|
|
302
|
+
def eval_S(self, exp, traza):
|
|
303
|
+
n = len(traza)
|
|
304
|
+
res = [None] * n
|
|
305
|
+
res_f = self.eval_formula(exp[e1], traza)
|
|
306
|
+
res_g = self.eval_formula(exp[e2], traza)
|
|
307
|
+
res[0] = res_g[0]
|
|
308
|
+
for i in range(1, n):
|
|
309
|
+
res[i] = self.eval_formula_in_event(
|
|
310
|
+
OR(res_g[i], AND(res_f[i], res[i - 1])), i, traza)
|
|
311
|
+
return res
|
|
312
|
+
|
|
313
|
+
def eval_O(self, exp, traza):
|
|
314
|
+
n = len(traza)
|
|
315
|
+
res = [None] * n
|
|
316
|
+
res_f = self.eval_formula(exp[e1], traza)
|
|
317
|
+
res[0] = self.eval_formula_in_event(res_f[0], 0, traza)
|
|
318
|
+
already_true = is_true(res[0])
|
|
319
|
+
for i in range(1, n):
|
|
320
|
+
if already_true: # O f holds at i-1, hence at every later event
|
|
321
|
+
res[i] = TRUE()
|
|
322
|
+
else:
|
|
323
|
+
res[i] = self.eval_formula_in_event(OR(res_f[i], res[i - 1]), i, traza)
|
|
324
|
+
already_true = is_true(res[i])
|
|
325
|
+
return res
|
|
326
|
+
|
|
327
|
+
def eval_H(self, exp, traza):
|
|
328
|
+
n = len(traza)
|
|
329
|
+
res = [None] * n
|
|
330
|
+
res_f = self.eval_formula(exp[e1], traza)
|
|
331
|
+
res[0] = self.eval_formula_in_event(res_f[0], 0, traza)
|
|
332
|
+
already_false = is_false(res[0])
|
|
333
|
+
for i in range(1, n):
|
|
334
|
+
if already_false: # H f fails at i-1, hence at every later event
|
|
335
|
+
res[i] = FALSE()
|
|
336
|
+
else:
|
|
337
|
+
res[i] = self.eval_formula_in_event(AND(res_f[i], res[i - 1]), i, traza)
|
|
338
|
+
already_false = is_false(res[i])
|
|
339
|
+
return res
|
|
340
|
+
|
|
341
|
+
# propositional operators and leaves
|
|
342
|
+
def eval_True_False(self, exp, traza):
|
|
343
|
+
return [[set(), exp[t]] for _ in traza]
|
|
344
|
+
|
|
345
|
+
def eval_Not(self, exp, traza):
|
|
346
|
+
res_f = self.eval_formula(exp[e1], traza)
|
|
347
|
+
return [self.eval_formula_in_event(NOT(res_f[i]), i, traza) for i in range(len(traza))]
|
|
348
|
+
|
|
349
|
+
def eval_AND(self, exp, traza):
|
|
350
|
+
res_f1 = self.eval_formula(exp[e1], traza)
|
|
351
|
+
res_f2 = self.eval_formula(exp[e2], traza)
|
|
352
|
+
return [self.eval_formula_in_event(
|
|
353
|
+
[res_f1[i][0] | res_f2[i][0], '&', res_f1[i], res_f2[i]], i, traza)
|
|
354
|
+
for i in range(len(traza))]
|
|
355
|
+
|
|
356
|
+
def eval_OR(self, exp, traza):
|
|
357
|
+
res_f1 = self.eval_formula(exp[e1], traza)
|
|
358
|
+
res_f2 = self.eval_formula(exp[e2], traza)
|
|
359
|
+
return [self.eval_formula_in_event(
|
|
360
|
+
[res_f1[i][0] | res_f2[i][0], '|', res_f1[i], res_f2[i]], i, traza)
|
|
361
|
+
for i in range(len(traza))]
|
|
362
|
+
|
|
363
|
+
def eval_fvar(self, exp, traza):
|
|
364
|
+
n = len(traza)
|
|
365
|
+
freezeVar = exp[e1]
|
|
366
|
+
newRow = self.eval_formula(exp[e2], traza)
|
|
367
|
+
newRow = [self.replace(newRow, i, newRow[i], freezeVar) for i in range(n)]
|
|
368
|
+
return [self.eval_formula_in_event(newRow[i], i, traza) for i in range(n)]
|
|
369
|
+
|
|
370
|
+
def eval_exp(self, exp, traza):
|
|
371
|
+
return [self.eval_formula_in_event(exp, i, traza) for i in range(len(traza))]
|
|
372
|
+
|
|
373
|
+
def eval_atom(self, exp, traza):
|
|
374
|
+
return [TRUE() if exp[e1] in event[I_ATOM] else FALSE() for event in traza]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
def results_statistics(res) -> tuple[int, int, int, float]:
|
|
380
|
+
"""Summarise the per-event results of a formula over one trace.
|
|
381
|
+
|
|
382
|
+
Returns ``(holds, true_count, false_count, ratio)`` where ``holds`` is 1
|
|
383
|
+
when the formula is true at the first event (the classic model checking
|
|
384
|
+
answer) and ``ratio`` is the fraction of events where it is true.
|
|
385
|
+
|
|
386
|
+
Raises ``ValueError`` if some event was left with an unresolved node.
|
|
387
|
+
"""
|
|
388
|
+
n = len(res)
|
|
389
|
+
trueCount = sum(1 for r in res if is_true(r))
|
|
390
|
+
falseCount = sum(1 for r in res if is_false(r))
|
|
391
|
+
if trueCount + falseCount != n:
|
|
392
|
+
raise ValueError(f"unresolved evaluation: {trueCount + falseCount} != {n}")
|
|
393
|
+
return (1 if is_true(res[0]) else 0), trueCount, falseCount, trueCount / n
|