Lesson 2.1: Macros, Aliases, and Anywhere Rules
The purpose of this lesson is to explain the behavior of the macro
,
macro-rec
, alias
, and alias-rec
production attributes, as well as the
anywhere
rule attribute. These attributes control the meaning of how rules
associated with them are applied.
Macros
Thus far in the K tutorial, we have described three different types of rules:
- Top-level rewrite rules, which rewrite a configuration composed of cells to another configuration;
- Function rules, which define the behavior of a function written over arbitrary input and output types; and
- Simplification rules, which describe ways in which the symbolic execution engine ought to simplify terms containing symbolic values.
This lesson introduces three more types of rules, the first of which are
macros. A production is a macro if it has the macro
attribute, and all
rules whose top symbol on the left hand side is a macro are macro rules
which define the behavior of the macro. Like function rules and simplification
rules, macro rules do not participate in cell completion. However, unlike
function rules and simplification rules, macro rules are applied statically
before rewriting begins, and the macro symbol is expected to no longer appear
in the initial configuration for rewriting once all macros in that
configuration are rewritten.
The rationale behind macros is they allow you to define one piece of syntax
in terms of another piece of syntax without any runtime overhead associated
with the cost of rewriting one to the other. This process is a common one in
programming language design and specification and is referred to as
desugaring; The syntax that is transformed is typically also referred to as
syntactic sugar for another type of syntax. For example, in a language with
if
statements and curly braces, you could write the following fragment
(lesson-01.k
):
kmodule LESSON-01 imports BOOL syntax Stmt ::= "if" "(" Exp ")" Stmt [macro] | "if" "(" Exp ")" Stmt "else" Stmt | "{" Stmts "}" syntax Stmts ::= List{Stmt,""} syntax Exp ::= Bool rule if ( E ) S => if ( E ) S else { .Stmts } endmodule
In this example, we see that an if statement without an else
clause is
defined in terms of one with an else
clause. As a result, we would only
need to give a single rule for how to rewrite if
statements, rather than
two separate rules for two types of if
statements. This is a common pattern
for dealing with program syntax that contains an optional component to it.
It is worth noting that by default, macros are not applied recursively. To be more precise, by default a macro that arises as a result of the expansion of the same macro is not rewritten further. This is primarily to simplify the macro expansion process and reduce the risk that improperly defined macros will lead to non-terminating behavior.
It is possible, however, to tell K to expand a macro recursively. To do this,
simply replace the macro
attribute with the macro-rec
attribute. Note that
K does not do any kind of checking to ensure termination here, so it is
important that rules be defined correctly to always terminate, otherwise the
macro expansion phase will run forever. Fortunately, in practice it is very
simple to ensure this property for most of the types of macros that are
typically used in real-world semantics.
Exercise
Using a Nat
sort containing the constructors 0
and S
(i.e., a
Peano-style axiomatization of the
natural numbers where S(N) = N + 1
, S(S(N)) = N + 2
, etc), write a macro
that will compute the sum of two numbers.
Aliases
NOTE: This lesson introduces the concept of "aliases", which are a variant of macros. While similar, this is different from the concept of "aliases" in matching logic, which is introduced in Lesson 2.16.
Macros can be very useful in helping you define a programming language. However, they can be disruptive while pretty printing a configuration. For example, you might write a set of macros that transforms the code the user wrote into equivalent code that is slightly harder to read. This can make it more difficult to understand the code when it is pretty printed as part of the output of rewriting.
K defines a relatively straightforward but novel solution to this problem, which is known as a K alias. An alias in K is very similar to a macro, with the exception that the rewrite rule will also be applied backwards during the pretty-printing process.
It is very simple to make a production be an alias instead of a macro: simply
use the alias
or alias-rec
attributes instead of the macro
or macro-rec
attributes. For example, if the example involving if
statements above was
declared using an alias instead of a macro, the Stmt
term if (E) {} else {}
would be pretty-printed as if (E) {}
. This is because during pretty-printing,
the term participates in another macro-expansion pass. However, this macro
expansion step will only apply rules with the alias
or alias-rec
attribute,
and, critically, it will reverse the rule by treating the left-hand side as if
it were the right-hand side, and vice versa.
This can be very useful to allow you to define one construct in terms of another while still being able to pretty-print the result as if it were the original term in question. This can be especially useful for applications of K where we are taking the output of rewriting and attempting to use it as a code fragment that we then execute, such as with test generation.
Exercise
Modify LESSON-01
above to use an alias instead of a macro and experiment
with how various terms are pretty-printed by invoking krun
on them.
anywhere
rules
The last type of rule introduced in this lesson is the anywhere rule. An
anywhere rule is specified by adding the anywhere
attribute to a rule. Such a
rule is similar to a function rule in that it does not participate in cell
completion, and will apply anywhere that the left-hand-side matches in the
configuration, but distinct in that the symbol in question can still be matched
against in the left-hand side of other rules, even during concrete rewriting.
The reasoning behind this is that instead of the symbol in question being a
constructor, it is a constructor modulo the axioms defined with the
anywhere
attribute. Essentially, the rules with the anywhere
attribute will
apply as soon as they appear in the right-hand side of a rule being applied,
but the symbol in question will still be treated as a symbol that can be
matched on if it is not completely removed by those rules.
This can be useful in certain cases to allow you to define transformations over
particular pieces of syntax while still generally giving those pieces of syntax
another meaning when the anywhere rule does not apply. For example, the ISO C
standard defines the semantics of *&x
as exactly equal to x
, with no
reading or writing of memory taking place, and the K semantics of C implements
this functionality using an anywhere rule that is applied at compilation time.
NOTE: the anywhere
attribute is only implemented on the LLVM backend
currently. Attempting to use it in a semantics that is compiled with the
Haskell backend will result in an error being reported by the compiler. This
should be remembered when using this attribute, as it may not be suitable for
a segment of a semantics which is intended to be symbolically executed.
Exercises
- Write a version of the calculator from Lesson 1.14 Exercise 1, which uses
the same syntax for evaluating expressions, but defines its arithmetic logic
using
anywhere
rules rather than top-level rewrite rules.
Return to Top
Click here to return to the Table of Contents for Section 2.