Rule Application
A detailed look at rules and rule application in K
In general, any semantics written in K works on a symbolic state of the form (C, PC)
, where:
C
is the configuration, which we can think of as a collection of cells; andPC
is the a path condition, which is a set of Boolean constraints on the symbolic variables present in the configuration
and allows the creator to use three flavours of rules:
rewrite rules, which describe the transitions of the semantics;
function evaluation rules (onward: function rules), which describe how functions are evaluated; and
simplifications, which allow one to use mathematical theorems to simplify terms.
For the purposes of this tutorial, we can assume that all three types of rules have the form:
where:
RI
andEI
, for1 <=Int I <=Int N
, are propositional formulae;the
LHS
of a rewrite rule does not contain function symbols;the
LHS
andRHS
of function rules and simplifications are propositional formulae;the top-level symbol of the
LHS
of a function rule or a simplification is a function symbol.
In principle, the rewrite rules can contain multiple rewrites (=>
), but as all can be recast to fit the above form there is no loss of generality.
Let us now understand what it means to apply a given rewrite rule to a given symbolic state (C, PC)
. This process consists of the following stages:
Matching: first, the backend understands whether or not the configuration
C
can be syntactically matched against theLHS
of the rule. This phase is purely syntactic and does not involve the SMT solver. If successful, it yields a substitutionS
, which maps symbolic variables in theLHS
to the corresponding parts ofC
. Otherwise, the rule cannot be applied. Contrast, for example, the following two rules, where the first rule requires a0
syntactically in second position, whereas the second rule requires there a term that is semantically equivalent to0
:
Requires: next, the backend applies the substitution
S
to the requires clause, obtaining the first-order constraintsS(RI)
, for1 <=Int I <=Int N
. Then, it checks, in unspecified order, whetherPC /\ S(RI)
is satisfiable, for eachRI
, fully applying simplifications and using the SMT solver. If this holds, the rule can be applied. Otherwise, it cannot.Application: next, the rule is actually applied, meaning that the part of the term
C
that was identified to match theLHS
of the rule is replaced byS(RHS)
.Ensures: finally, the substitution is applied to the
ensures
clause, obtainingS(EI)
, for1 <=Int I <=Int M
, which are then fully simplified and the non-trivial ones added to the path condition. Finally, an SMT-check is performed to understand whether or not this has made the path condition unsatisfiable, and if this is the case, the obtained symbolic state is marked as vacuous.
One full rewrite step of a proof generalises the above process in roughly the following way:
It takes the current symbolic state
(C, PC)
and attempts to apply all of the rewrite rules available from the language definition, in order of priority.If no rules are applicable:
If no lower priority rules are available, the state is marked as stuck.
Otherwise, step 1 is repeated with the next set of lower priority rules.
Otherwise, let there be
M
applicable rules, let their requires clauses beRI
, for1 <=Int I <=Int M
, and let the obtained substitutions beSI
, for1 <=Int I <=Int M
. The backend then checks whether or not the applicable rules cover the space of possibilities, that is, whether or notPC /\ !(S1(R1) \/ ... \/ SM(RM))
, that is,PC /\ !S1(R1) /\ ... /\ !SM(RM)
is satisfiable. This path condition characterises what is known as the remainder branch. If this branch is feasible, the process is repeated for it, but only for the lower priority rules.
Like rewrite rules, simplifications and function evaluation rules are applied in order of priority. However, they are applied on a first-success basis and their requires
clauses are checked using entailment, not satisfiability. These rules are meant to fire only if we know with certainty that they apply, that is, they are not meant to create branches in the proof.
An example of rule application
Let us consider the following symbolic state
and try to apply the following rules:
We start by trying to apply the higher priority rules, exec-b-01
and exec-b-02
, at priority 40. Both rules apply, with the same substitution
and the corresponding substituted requires clauses:
Next, we check if the space of possibilities is covered by computing the remainder condition PC /\ !S1(R1) /\ S2(R2)
:
which is equivalent to
and which is satisfiable, which means that we have to try the exec-b-03
rule as well, which also can be applied, with the substitution:
and the substituted requires clause
This time, the remainder condition equals
but that is not satisfiable, meaning that the three rules cover the space of possibilities given the initial symbolic state. This means that we end up with three branches, resulting in the three following symbolic states:
Aside: writing a well-formed K semantics
There are two principles that should be followed in order for a K semantics to be well-formed, that is, that starting from a well-defined initial symbolic state and executing the associated rewrite rules, function evaluation rules, and simplifications can never end up in an undefined state:
All rewrite rules must cover the entire space of possibilites. Some examples of this coverage from KEVM are the rules for the
JUMPI
instruction:
where the space of possibilities is split into two at the level of the path condition, and the rules for the BALANCE
instruction:
where the space of possibilities is split into two at the level of the configuration, with the help of the owise
attribute.
Any rewrite rules, function rules, or simplifications that use partial functions on their RHS (for example,
/Int
andmodInt
) must ensure that the parameters of said functions are within their domain. In addition, all of these rules must be marked with thepreserves-definedness
attribute.
Last updated