Operational Semantics
Introduction
The operational semantics of a language should answer the question what an expression will evaluate to. The description should not leave any room for interpretation since the description is what defines the language.
The operational semantics should preferably be described in a way that it also captures the time and memory complexity of an execution. It does not have to be a detailed description of how things are actually implemented but it should give an understanding of the execution to allow a programmer to reason about the efficiency of a particular program.
The operational semantics of a language can also serve as a architecture for an abstract machine for the language or as the design criteria for a compiler. The observable properties of a program execution should conform to the properties one can derive from the description of the operational semantics.There are many ways to describe an operational semantics of a programming language and we will use a strategy called big-step semantics. We will describe the semantics as a set of rewrite rules or evaluation relations; given an expression in the language, we will describe the rules of how to evaluatethe expression to obtain an answer.This description of an operational semantics for our small functional programming language will serve our purposes in that we will be able to talk and reason about program execution. We will also be able to use it when we implement an interpreter for the language.
The language
Our language is a very small functional programming language that should behave similar to Elixir. We will in the beginning only have a handful of data types and program constructions but will then extend the language to look more like a proper language. To start with our language will only consist of the constructs to express a sequence of pattern matching expressions followed by a single expression.
The expressions that we allow are simple term expressions i.e. no function calls, case nor if-expressions. We define this language using a BNF grammar.We assume that we have two lexical constructs, atoms and variables, where atoms are written with an initial colon (:). We also have one compound expression called a cons, a simple binary construct that holds two expressions.
〈expression〉::=〈atom〉| 〈variable〉| ’{’〈expression〉’,’〈expression〉'}'
If this was the whole language we would only be able to write expressions like :foo
, x
and :zot.
This would not be that interesting so we add the the description of a sequence.
〈sequence〉::=〈expression〉| 〈match〉’;’〈sequence〉〈match〉::=〈pattern〉’=’〈expression
〉
A pattern matching expression consists of a pattern and an expressions,and the pattern will look almost exactly as an expression. The only difference is that we allow’’which will be called don’t care.
〈pattern〉::=〈atom〉| 〈variable〉|’’|’{’〈pattern〉’,’〈pattern〉’}’
An expression is just a syntactical construct, the grammar presented does not give any meaning to the expressions that we can write but it defines which sequences of characters are legal sequences, patterns and expressions.
the domain
The domain is the set of data structures that will be the result of our computation. It is the set of all Atoms and compound data structures that we can generate from elements in the domain.
Atoms ={a,b,c,...}
Structures = Atoms ∪ {{d1,d2}|di ∈ Structures}
We have a one-to-one mapping from our atoms in the language and the atoms in the domain. You might wonder if they are not the same but `there is a difference between expressions in our language and elements in our domain. Think about the written number “12” or in Roman “XII” and the number twelve. We will have expression that look like
{:foo , {:bar , :zot}}` and data structures that we write {foo, {bar, zot}}. If everything works out fine, the evaluation of an expression will return the corresponding data structure.
an environment
In the description of the evaluation we will need an environment. This is a mapping from variables in expressions to elements in the domain. We will start with an empty environment and gradually add more information as we evaluate a sequence of pattern matching expressions. An environment is represented as a set of bindings v/s. Here v is a variable (and we will typically use v or x, y etc when we talk about variables) and s is a data structure. An environment that binds x to a and y to b would then be written.
{x/a,y/b}
The evaluation function
So we’re now ready to describe the operational semantics of our programming language and we do this by describing how an expressions is evaluated.We will start with the simplest expressions and then work our way up to more complex expressions.The evaluation of an expression e is written Eσ(e), meaning that we evaluate the expression in the context of the environment σ. When we describe our rules for evaluation we will use a notation that is:
In this description E is the evaluation function, we will also have rules for other functions, and σ an environment. The result of applying the function is a data structure or ⊥. The symbol ⊥ represents the failed execution.
In order to apply the rule the prerequisite must be met and this will in the end guide us in how we can implement a recursive evaluator. The so called big-step operational semantics is often used since it is easily turned into an implementation of the language.
atoms, variables and tuples
The simplest rule is the one that describes how we evaluate an expression consisting of a simple atom.
A variable is of course different since we then need to consult the environment for a binding of the variable.
The rule for a compound expression {:foo, :bar}
is straight forward Given that we can evaluate its two components we will of course be able to evaluate the compound expression.
a pattern matching expression
The first two rules are simple; an atom will of course match its corresponding data structure and the don't care symbol will match anything.
If we try to match an atom to a data structure that is not the corresponding data structure then we fail.
If we have a unbound variable as a pattern then the variable is bound to a structure in the environment.
If the variable is bound to the equivalent structure we proceed without extending the environment but if it is bound to something else we fail.
We here implicitly state that the pattern matching succeeds. If the first matching fails the matching of the whole structure fails.
The same holds if the second matching fails.
As an exercise you should do the pattern matching of the expression {x, {x, :c}}
and the data structure {a, {b, c}}. Note how we first add x/a as a binding and then fail when we match {x, :c}
and {b,c}.
The remaining alternative, the case where we have a cons expression and we try to match this to an data structure that is not a compound data structure, will of course lead to a failure.
That it is as far as pattern matching goes. Try to do some matching by hand and explain which rules you apply. Try these:
a sequence
So now we're ready to evaluate a sequence; a sequence that always consist of zero or more pattern matching expressions followed by a single expression. The pattern matching expressions will, if they succeed, add more bindings to the environment as we proceed and the final expression is then evaluated given this environment.
In order to describe this we introduce a new rule, a rule that describes how a new scope is created.
that's it
That is it, we now have all the rules to evaluate a sequence on the form shown in the beginning. Make sure that you understand the rules and how they are applied by evaluating the sequence by hand. If you get it right the result will be {foo, {bar, nil}}. When you get it right you're ready to continue.
Adding a case expression
The expressions that we have seen so far are rather boring. In order to write a program that is at lest marginally interesting we need a construct that evaluates to different data structures depending on the state of the execution. We could have introduced a if-then-else expression but we choose to introduce a so called case expression.
We first need to extend the grammar so that we have a syntax to express our new construct. We choose a syntax that is similar to the case expression in Erlang.
The right clause is selected by trying to match the pattern of the first clause with the data structure obtained by evaluating the expression. If the pattern matching succeeds we will continue to evaluate the sequence of the clause, in the extended environment. Note that we also here create new scopes for the variables in the pattern.
This rule could of course also be used even if clauses is empty i.e. we match the last or only clause in a sequence.
If the pattern matching fails we will simply try the next clause in the sequence of clauses.
We now have everything we need to handle case expressions in our language, this is starting to look like something.
Adding lambda expressions
The real task is when we want to add lambda expressions, or unnamed functions. To do this wee need to do several things. We need to extend the syntax to represent a function and to apply a function to a sequence of arguments. We also need to add a new data structure to represent a function and, extend the rules of evaluation to give everything a meaning.
free variables
We want to know which variables in the function expression that are free. To see the problem let's look at the function expression that has a match expression in the sequence.
fn (x) -> y = 5; x + y + z end
Which variables are free in this expression? The variable x
is not free since it is in the scope of the function parameter. Nor is the local variable y
free since it is in the scope of the pattern matching expression. If you would translate this to lambda calculus the expression would look like follows:
The variable z
is however free and in order to make use of this function expression one would have to do it in an environment where z
has a value. A function and the needed environment, i.e. values for all free variables, is called a closure. We need to introduce new constructs in our language to create closures and apply them to arguments.
function expression and application
So we introduce two new constructs in our language, one to express a function and one to apply a function to a sequence of arguments. Different from Elixir we don't allow patterns in function parameters; this is only to make the rules of the evaluation easier to describe.
A function consist of the keyword fn
followed by a, possibly empty, sequence of parameters (all unique variables). After the arrow we have a regular sequence as we have defined before and everything is finished by the keyword end
.
A function application is simply any expression (that hopefully will be evaluated to a {\em closure} and a sequence of arguments enclosed in parentheses. We here follow the Elixir syntax that requires a '.' between the name less function and the sequence of arguments. The arguments can of course be arbitrary expressions.
closures
The next thing we need to do is to extend our set of data structures. When we evaluate a function expression we will construct the closure. The closure is a triplet: the parameters of the function, the sequence to evaluate and an environment.
We have not formally defined what the set of parameters, sequences nor environments are but the informal description will work for our purposes.
The environment of a closure is constructed by taking the bindings of all free variables in the sequence, not including the variables that are bound by the parameters.
applying a closure
So now to the interesting part where we apply a closure to a sequence of arguments. We first need to evaluate the arguments and then add a set of bindings to the environment of the closure. We then evaluate the sequence in the updated environment.
Looks complicated but it's quite straight forward. Go through the evaluation of sequence shown below. If everything works out fine the result should be {foo, bar}.
An interpreter
The big-step operational semantics that we have used in describing the language is very useful when one wants to implement an interpreter for the language. If we can only come up with a scheme to represent expressions, data structures and environments then the rules will give us a recursive interpreter with very little effort.
Last updated