ADR-023: Trace evaluation
authors | last revised |
---|---|
Jure Kukovec | 2022-09-28 |
Table of Contents
- Summary (Overview)
- Context (Problem)
- Options (Alternatives)
- Solution (Decision)
- Consequences (Retrospection)
Summary
In the context of improving usability
facing difficulties understanding counterexample traces
we decided to implement trace evaluation
to achieve a better user experience
accepting the development costs.
Context
As explained in #1845, one often runs into the problem of unreadable counterexamples;
for complex specifications, it is often the case that either the state contains many variables, or the invariant contains many conjunctive components (or both).
In that case, trying to determine exactly why e.g. A1 /\ ... /\ An
was violated basically boils down to manually evaluating each formula Ai
with whatever variables are present in the state.
This is laborious and error-prone, and should be automated.
Options
- Call REPL in each state:
- No convenient REPL integration at the moment
- No clear way of saving outputs
- Encode trace traversal as an Apalache problem and use the solver
- No additional rules or IO needed
Solution
We choose option (2).
Suppose we are given a trace s_0 -> s_1 -> ... -> s_n
over variables x_1,...,x_k
as well as expressions E_1,...,E_m
, such that all free variables of E_1,...,E_m
are among x_1,...,x_k
. W.l.o.g. assume all constants are inlined.
The above defines a trace t_0 -> t_1 -> ... -> t_n
over variables v_1,...,v_m
, such that
t_i.v_j = E_j[s_i.x_1/x_1,...,s_i.x_k/x_k]
for all i \in 0..n, j \in 1..m
. In other words, v_j
in state t_i
is the evaluation of the expression E_j
in state s_i
.
By using transition filtering instead of a generic Next-decomposition, this can be encoded as a specification free of control-nondeterminism, in-state computation, or invariants, and is thus incredibly efficient to represent in SMT.
Then, the solver will naturally return an ITF trace containing the evaluations of E_1,...,E_m
in each state s_0,...,s_n
(the values of v_1,...,v_m
).
Input
The invocation of the trace evaluation command should look like this:
$ apalache-mc tracee --trace=<trace> --expressions=<exprs> <source>
where:
<trace>
is a trace produced by apalache, in either.tla
,.json
or.itf.json
formats<exprs>
is a comma-separated list of expression names, to be evaluated over the trace provided by--trace
Note that <source>
can just be the file used to produce the trace in the first place.
Output
The above command should produce an Apalache trace (in all available formats), with the following properties:
- The output trace length is equal to the input trace length
- If
--expressions=E_1,...,E_m
is used, the variables of the output trace areE_1,...,E_m
. - For all
i,j
, the value ofE_i
in statej
of the output trace is equal to the evaluation ofE_i
, as defined in<source>
, using the values the variables of the input trace hold in statej
of the input trace.
Recall that the output trace will only display the expressions E_1,...E_m
as the output state variables. Should you wish to view the original trace variables, you need to add an expression, like one of the ones below for example:
E_single == x_1
E_state == [ x1 |-> x_1, ..., xk |-> x_k ]
E_vars == <<x_1, ..., x_k>>
Optionally, we could investigate one of the following two alternatives to the output format:
- The output trace variables are
x_1,...,x_k,E_1,...,E_m
instead, wherex_1,...,x_k
are the variables of the original trace. The value of each variable from the input trace has the same value in every state of the output trace, as it does in the corresponding state of the input trace. This is perhaps preferable to use with the ITF trace viewer. - The output contains both the input trace and the output trace (as it would have been produced in the original suggestion) in the same file, but separately.
Example
Assume we are given the source test.tla
Source
-------------------------- MODULE test -----------------------------
EXTENDS Integers
VARIABLE
\* @type: Int;
x
A == x * x
B == IF x < 3 THEN 0 ELSE 1
C == [y \in {1,2,4} |-> {y} ][x]
D == x % 2 = 0
Init == x = 1
Next == x' = x + 1
Inv == TRUE
=========================================================================
and trace testTrace.json
(length 5, x=1 -> ... -> x=5
).
Trace
{
"name": "ApalacheIR",
"version": "1.0",
"description": "https://apalache-mc.org/docs/adr/005adr-json.html",
"modules": [
{
"kind": "TlaModule",
"name": "counterexample",
"declarations": [
{
"type": "() => Bool",
"kind": "TlaOperDecl",
"name": "ConstInit",
"formalParams": [
],
"isRecursive": false,
"body": {
"type": "Untyped",
"kind": "ValEx",
"value": {
"kind": "TlaBool",
"value": true
}
}
},
{
"type": "() => Bool",
"kind": "TlaOperDecl",
"name": "State0",
"formalParams": [
],
"isRecursive": false,
"body": {
"type": "Bool",
"kind": "OperEx",
"oper": "AND",
"args": [
{
"type": "Bool",
"kind": "OperEx",
"oper": "EQ",
"args": [
{
"type": "Int",
"kind": "NameEx",
"name": "x"
},
{
"type": "Int",
"kind": "ValEx",
"value": {
"kind": "TlaInt",
"value": 1
}
}
]
}
]
}
},
{
"type": "() => Bool",
"kind": "TlaOperDecl",
"name": "State1",
"formalParams": [
],
"isRecursive": false,
"body": {
"type": "Bool",
"kind": "OperEx",
"oper": "AND",
"args": [
{
"type": "Bool",
"kind": "OperEx",
"oper": "EQ",
"args": [
{
"type": "Int",
"kind": "NameEx",
"name": "x"
},
{
"type": "Int",
"kind": "ValEx",
"value": {
"kind": "TlaInt",
"value": 2
}
}
]
}
]
}
},
{
"type": "() => Bool",
"kind": "TlaOperDecl",
"name": "State2",
"formalParams": [
],
"isRecursive": false,
"body": {
"type": "Bool",
"kind": "OperEx",
"oper": "AND",
"args": [
{
"type": "Bool",
"kind": "OperEx",
"oper": "EQ",
"args": [
{
"type": "Int",
"kind": "NameEx",
"name": "x"
},
{
"type": "Int",
"kind": "ValEx",
"value": {
"kind": "TlaInt",
"value": 3
}
}
]
}
]
}
},
{
"type": "() => Bool",
"kind": "TlaOperDecl",
"name": "State3",
"formalParams": [
],
"isRecursive": false,
"body": {
"type": "Bool",
"kind": "OperEx",
"oper": "AND",
"args": [
{
"type": "Bool",
"kind": "OperEx",
"oper": "EQ",
"args": [
{
"type": "Int",
"kind": "NameEx",
"name": "x"
},
{
"type": "Int",
"kind": "ValEx",
"value": {
"kind": "TlaInt",
"value": 4
}
}
]
}
]
}
},
{
"type": "() => Bool",
"kind": "TlaOperDecl",
"name": "State4",
"formalParams": [
],
"isRecursive": false,
"body": {
"type": "Bool",
"kind": "OperEx",
"oper": "AND",
"args": [
{
"type": "Bool",
"kind": "OperEx",
"oper": "EQ",
"args": [
{
"type": "Int",
"kind": "NameEx",
"name": "x"
},
{
"type": "Int",
"kind": "ValEx",
"value": {
"kind": "TlaInt",
"value": 5
}
}
]
}
]
}
},
{
"type": "() => Bool",
"kind": "TlaOperDecl",
"name": "InvariantViolation",
"formalParams": [
],
"isRecursive": false,
"body": {
"type": "Bool",
"kind": "ValEx",
"value": {
"kind": "TlaBool",
"value": true
}
}
}
]
}
]
}
After running tracee --trace=testTrace.json --expressions=A,B,C,D test.tla
, we should see
...
Constructing an example run I@16:06:59.450
Check the trace in: <PATH>/example0.tla, ... I@16:06:59.563
The outcome is: NoError I@16:06:59.571
Trace successfully generated.
where example0.tla
looks like
Result
---------------------------- MODULE counterexample ----------------------------
EXTENDS test
(* Constant initialization state *)
ConstInit == TRUE
(* Initial state *)
State0 == A = 1/\ B = 0/\ C = {1}/\ D = FALSE
(* Transition 0 to State1 *)
State1 == A = 4/\ B = 0/\ C = {2}/\ D = TRUE
(* Transition 1 to State2 *)
State2 == A = 9/\ B = 1/\ C = {}/\ D = FALSE
(* Transition 2 to State3 *)
State3 == A = 16/\ B = 1/\ C = {4}/\ D = TRUE
(* Transition 3 to State4 *)
State4 == A = 25/\ B = 1/\ C = {}/\ D = FALSE
(* The following formula holds true in the last state and violates the invariant *)
InvariantViolation == TRUE
================================================================================
(* Created by Apalache on Mon Oct 17 16:06:59 CEST 2022 *)
(* https://github.com/informalsystems/apalache *)
Consequences
TBD