Programming
Programming
Version 1.0.4
Mike Grant
Zachary Palmer
Scott Smith
[Link]
i
1 Introduction 1
2 Operational Semantics 3
2.1 A First Look at Operational Semantics . . . . . . . . . . . . . . . . . . . . 3
2.2 BNF grammars and Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.2.1 Operational Semantics for Logic Expressions . . . . . . . . . . . . 5
2.2.2 Abstract Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.3 Operational Semantics and Interpreters . . . . . . . . . . . . . . . 12
2.3 The F♭ Programming Language . . . . . . . . . . . . . . . . . . . . . . . . 14
2.3.1 F♭ Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.3.2 Variable Substitution . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.3.3 Operational Semantics for F♭ . . . . . . . . . . . . . . . . . . . . . 21
2.3.4 The Expressiveness of F♭ . . . . . . . . . . . . . . . . . . . . . . . 28
2.3.5 Russell’s Paradox and Encoding Recursion . . . . . . . . . . . . . 32
2.3.6 Call-By-Name Parameter Passing . . . . . . . . . . . . . . . . . . . 38
2.3.7 F♭ Abstract Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.4 Operational Equivalence . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.4.1 Defining Operational Equivalence . . . . . . . . . . . . . . . . . . . 43
2.4.2 Properties of Operational Equivalence . . . . . . . . . . . . . . . . 45
2.4.3 Examples of Operational Equivalence . . . . . . . . . . . . . . . . 46
2.4.4 The λ-Calculus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
ii
CONTENTS iii
7 Concurrency 130
7.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
7.1.1 The Java Concurrency Model . . . . . . . . . . . . . . . . . . . . . 131
7.2 The Actor Model and AF♭V . . . . . . . . . . . . . . . . . . . . . . . . . 132
7.2.1 The Syntax of AF♭V . . . . . . . . . . . . . . . . . . . . . . . . . 133
7.2.2 An Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
7.2.3 Operational Semantics of Actors . . . . . . . . . . . . . . . . . . . 135
7.2.4 The Local Rules . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
7.2.5 The Global Rule . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
7.2.6 The Atomicity of Actors . . . . . . . . . . . . . . . . . . . . . . . . 137
Bibliography 159
Index 161
Chapter 1
Introduction
In this book, our goal is to study the fundamental concepts in programming languages,
as opposed to learning a range of specific languages. Languages are easy to learn, it
is the concepts behind them that are difficult. The basic features we study in turn in-
clude higher-order functions, data structures in the form of records and variants, mutable
state, exceptions, objects and classes, and types. We also study language implementa-
tions, both through language interpreters and language compilers. Throughout the book
we write small interpreters for toy languages, and in Chapter 8 we write a principled
compiler. We define type checkers to define which programs are well-typed and which
are not. We also take a more precise, mathematical view of interpreters and type check-
ers, via the concepts of operational semantics and type systems. These last two concepts
have historically evolved from the logician’s view of programming.
The material has evolved from lecture notes used in a programming languages course
for juniors, seniors, and graduate students at Johns Hopkins University [21].
While the book uses formal mathematical techniques such as operational semantics
and type systems, it does not emphasize proofs of properties of these systems. We will
nonetheless sketch the intuitions of some proofs.
The FbDK
Complementing the book is the F♭ Development Kit, FbDK. It is a set of OCaml utilities
and interpreters for designing and experimenting with the toy F♭ and F♭SR languages
defined in the book. It is available from the book homepage at [Link]
pl/book.
1
CHAPTER 1. INTRODUCTION 2
Background Needed
The book assumes familiarity with the basics of OCaml, including the module system
(but not the objects, the “O” in OCaml). Beyond that there is no absolute prerequisite,
but knowledge of C, C++, and Java is helpful because many of the topics in this book are
implemented in these languages. The compiler presented in chapter 8 produces C code
as its target, and so a basic knowledge of C will be needed to implement the compiler.
More nebulously, a certain “mathematical maturity” greatly helps in understanding the
concepts, some of which are deep. for this reason, previous study of mathematics, for-
mal logic and other foundational topics in Computer Science such as automata theory,
grammars, and algorithms will be a great help.
Now, make sure your seat belts are buckled, sit back, relax, and enjoy the ride. . .
Chapter 2
Operational Semantics
3
CHAPTER 2. OPERATIONAL SEMANTICS 4
where each “form” above describes a particular language form – that is, a string of
terminals and non-terminals. A term in the language is a string of terminals which
matches the description of one of these rules (traditionally the first).
For example, consider the language Sheep. Let {S} be the set of nonterminals, {a, b}
be the set of terminals, and the grammar definition be:
S ::= b | Sa
That is, any string starting with the character b and followed by zero or more a characters
is a term in Sheep. The following are examples that are not terms in SHEEP:
S b
S a
The above syntax diagram describes all terms of the Sheep language. To generate
a form of S, one starts at the left side of the diagram and moves until one reaches the
right. The rectangular nodes represent non-terminals while the rounded nodes represent
terminals. Upon reaching a non-terminal node, one must construct a term using that
non-terminal to proceed.
CHAPTER 2. OPERATIONAL SEMANTICS 5
As another example, consider the language Frog. Let {F, G} be the set of nontermi-
nals, {r, i, b, t} be the set of terminals, and the grammar definition be:
F ::= rF | iG
G ::= bG | bF | t
Note that this is a mutually recursive definition. Note also that each production rule
defines a syntactic category. Terms in FROG include:
• rbt: When a term in Frog starts with r, the following non-terminal is F . The
non-terminal F may only be exapnded into rF or iG, neither of which start with
b. Thus, no string starting with rb is a term in Frog.
• bit: The only forms starting with b appear as part of the definition of G. As F is
the first non-terminal defined, terms in Frog must match F (which does not have
any forms starting with b).
F r F
i G
G b G
b F
because it is the syntax that describes the textual representation of an expression in the
language. We can express it in a BNF grammar as follows:
e v
Not e
e And e
e Or e
e Implies e
v True
False
Note that the syntax above breaks tradition somewhat by using lower-case letters
for non-terminals. Terminals are printed in fixed-width font. The rationale for this is
consistency with the metavariables we will be using in operational semantics below and
will become clear shortly.
We can now discuss the operational semantics of the boolean language. Operational
semantics are written in the form of logic rules, which are written as a series of pre-
conditions above a horizontal line and the conclusion below it. For example, the logic
rule
Red(x) Shiny(x)
(Apple Rule)
Apple(x)
indicates that if a thing is red and shiny, then that thing is an apple. This is, of course, not
true; many red, shiny things exist which are not apples. Nonetheless, it is a valid logical
statement. In our work, we will be defining logical rules pertaining to a programming
language; as a result, we have control over the space in which the rules are constructed.
We need not necessarily concern ourselves with intuitive sense so long as the programming
language has a mathematical foundation.
Operational semantics rules discuss how pieces of code evaluate. For example, let us
consider the And rule. We may define the following rule for And:
This rule indicates that the boolean language code True And False evaluates to
False. The absence of any preconditions above the line means that no conditions must
be met; this operational semantics rule is always true. Rules with nothing above the line
are termed axioms since they have no preconditions and so the conclusion always holds.
As a rule, though, it isn’t very useful. It only evaluates a very specific program.
This rule does not describe how to evaluate the program True And True, for instance.
In order to generalize our rules to describe a full language and not just specific terms
within the language, we must make use of metavariables.
To maintain consistency with the above BNF grammar, we use metavariables starting
with e to represent expressions and metavariables starting with v to represent values.
We are now ready to make an attempt at describing every aspect of the And operator
using the following new rule:
Using this rule, we can successfully evaluate True And False, True and True, and
so on. Note that we have used a textual description to indicate the value of the expression
v1 And v2 ; this is permitted, although most rules in more complex languages will not
use such descriptions.
We very quickly encounter limitations in our approach, however. Consider the pro-
gram True And (False And True). If we tried to apply the above rule to that program,
we would have v1 = True and v2 = (False And True). These two values cannot be ap-
plied to logical and as (False and True) is not a boolean value; it is an expression.
Our boolean language rule does not allow for cases in which the operands to And are
expressions. We therefore make another attempt at the rule:
e1 ⇒ v1 e2 ⇒ v2
(And Rule (Try 3))
e1 And e2 ⇒ the logical and of v1 and v2
This rule is almost precisely what we want; in fact, the rule itself is complete. Intu-
itively, this rule says that e1 And e2 evaluates to the logical and of the values represented
by e1 and e2 . But consider again the program True And False, which we expect to
evaluate to False. We can see that e1 = True and that e2 = False, but our evaluation
relation does not relate v1 or v2 to any value. This is because, strictly speaking, we do
not know that True ⇒ True.
Of course, we would like that to be the case and, since we are in the process of
defining the language, we can make it so. We simply need to declare it in an operational
semantics rule.
(Value Rule)
v⇒v
The value rule above is an axiom declaring that any value always evaluates to itself.
This satisfies our requirement and allows us to make use of the And rule. Using this
CHAPTER 2. OPERATIONAL SEMANTICS 8
formal logic approach, we can now prove that True And (False And True) ⇒ False
as follows:
One may read the above proof tree as an explanation as to why True And (False
And True) evaluates to False. We can choose to read that proof as follows: “True And
(False And True) evaluates to False by the And rule because we know True evaluates
to True, that False And True evaluates to False, and that the logical and of true and
false is false. We know that False And True evaluates to False by the And rule because
True evaluates to True, False evaluates to False, and the logical and of true and false
is false.”
An equivalent and similarly informal format for the above is:
True And (False And True) ⇒ False, because by the And rule
True ⇒ True, and
(False And True) ⇒ False, the latter because
True ⇒ True, and
False ⇒ False
The important thing to note about all three of these representations is that they are
describing a proof tree. The proof tree consists of nodes which represent the application
of logical rules with preconditions as their children. To complete our boolean language,
we define the ⇒ relation using a complete set of operational semantics rules:
(Value Rule)
v⇒v
e⇒v
(Not Rule)
Not e ⇒ the negation of v
e1 ⇒ v1 e2 ⇒ v2
(And Rule)
e1 And e2 ⇒ the logical and of v1 and v2
The rules for Or and Implies are left as an exercise to the reader (see Exercise 2.4).
These rules form a proof system as is found in mathematical logic. Logical rules
express incontrovertible logical truths. A proof of e ⇒ v amounts to constructing a
sequence of rule applications such that, for any given application of a rule, the items
above the line appeared earlier in the sequence and such that the final rule application
is e ⇒ v. A proof is structurally a tree, where each node is a rule, and the subtree rules
have conclusions which exactly match what the parent’s assumptions are. For a proof
tree of e ⇒ v, the root rule has as its conclusion e ⇒ v. Note that all leaves of a proof
tree must be axioms. A tree with a non-axiom leaf is not a proof.
CHAPTER 2. OPERATIONAL SEMANTICS 9
Notice how the above proof tree is expressing how this logic expression could be
computed. Proofs of e ⇒ v corresponds closely to how the execution of e produces the
value v as result. The only difference is that “execution” starts with e and produces the
v, whereas a proof tree describes a relation between e and v, not a function from e to v.
Lemma 2.2. The boolean language is normalizing: For all boolean expressions e, there
is some value v where e ⇒ v.
When a proof e ⇒ v can be constructed for some program e, we say that e converges.
When no such proof exists, e diverges. Because the boolean language is normalizing, all
programs in that language are said to converge. Some languages (such as OCaml) are not
normalizing; there are syntactically legal programs for which no evaluation proof exists.
An example of a OCaml program which is divergent is let rec f x = f x in f 0;; .
type boolexp =
True | False |
Not of boolexp |
And of boolexp * boolexp |
Or of boolexp * boolexp |
Implies of boolexp * boolexp;;
To understand how the abstract and concrete syntax relate, consider the following
examples:
CHAPTER 2. OPERATIONAL SEMANTICS 10
Example 2.1.
Concrete:
True
True
Abstract:
True
Example 2.2.
Concrete:
True And False And
Concrete: Implies
(True And False) Implies
((Not True) And False) And And
There is a simple and direct relationship between the concrete syntax of a language
and the abstract syntax. As mentioned above, the abstract syntax is a form which more
directly represents the operations being performed whereas the concrete syntax is the
form in which the operations are actually expressed. Part of the process of compiling
or interpreting a program is to translate the concrete syntax term (source file) into an
abstract syntax term (AST) in order to manipulate it. We define a relation JcK = a
to map concrete syntax form c to abstract syntax form a (in this case for the boolean
language):
JTrueK = True
JFalseK = False
JNot eK = Not(e)
Je1 And e2 K = And(Je1 K, Je2 K)
Je1 Or e2 K = Or(Je1 K, Je2 K)
Je1 Implies e2 K = Implies(Je1 K, Je2 K)
The grammar we give is ambiguous in that there are multiple parse trees for some
concrete expressions, but we implicitly assume the usual operator precedence applies
with And binding tighter than Or binding tighter than Implies. Consider the following
examples:
Example 2.4.
Concrete: And
(True Or True) And False
Or False
Abstract:
And(Or(True,True),False) True True
Example 2.5.
Concrete: Or
True Or (True And False)
True And
Abstract:
Or(True,And(True,False)) True False
The expression in example 2.4 will evaluate to False because one must evaluate the Or
operation first and then evaluate the And operation using the result. Example 2.5, on the
other hand, performs the operations in the opposite order. Note that in both examples,
though, the parentheses themselves are no longer overtly present in the abstract syntax.
This is because they are implicitly represented in the structure of the AST; that is, the
AST in example 2.5 would not have the shape that it has if the parentheses were not
present in the concrete syntax of the form.
CHAPTER 2. OPERATIONAL SEMANTICS 12
In short, parentheses merely change how expressions are grouped. In example 2.5, the
only rule we can match to the entire expression is the Or rule; the And rule obviously can’t
match because the left parentheses would be part of e1 while the right parenthesis would
be part of e2 (and expressions with unmatched parentheses make no sense). Similarly but
less obviously, example 2.4 can only match the And rule; the associativity implicitly forces
the Or rule to happen first, giving the And operator that entire expression to evaluate.
This distinction is clearly and correspondingly represented in the ASTs of the examples,
a fact which is key to the applicability of operational semantics.
in our function is a faithful implementation of Try 2 of our And rule, a rule which we
rejected precisely because it could not handle nested expressions.
How can we correct this problem? We are trying to write a faithful implementation of
our final And rule, which relies on the evaluation of the And rule’s operands. Thus, in our
implementation, we must evaluate those operands; we make this possible by declaring
our evaluation function to be recursive.
let rec eval exp =
match exp with
| True -> True
| False -> False
| And(exp0,exp1) ->
begin
match (eval exp0, eval exp1) with
| (True,True) -> True
| (_,False) -> False
| (False,_) -> False
end
Observe that, in the above code, we have changed very little. We modified the eval
function to be recursive. We also added a call to eval for each of the operands to the
And operation. That call alone is sufficient to fix the problem; the process of evaluating
those arguments represents the e1 ⇒ v1 and e2 ⇒ v2 preconditions on the And rule, while
the use of the resultings values in the tuple causes the match to be against v1 and v2
rather than e1 and e2 . The above code is a faithful implementation of the value rule and
the And rule.
We can now complete the boolean language interpreter by continuing the eval fuction
in the same form:
let rec eval exp =
match exp with
True -> True
| False -> False
| Not(exp0) -> (match eval exp0 with
True -> False
| False -> True)
| And(exp0,exp1) -> (match (eval exp0, eval exp1) with
(True,True) -> True
| (_,False) -> False
| (False,_) -> False)
Metacircular interpreters give you some idea of how a language works, but suffer
from the non-foundational problems implied in Exercise 2.5. A metacircular interpreter
for Lisp (that is, a Lisp interpreter written in Lisp) is a classic programming language
theory exercise.
• If the algorithm is given arguments for which the function is defined, it must produce
the correct answer within a finite amount of time.
• If the algorithm is given arguments for which the function is not defined, it must
either produce a clear error or otherwise not terminate. (That is, it must not appear
to have produced an incorrect value for the function if no such value is defined.)
The above definition of a partial recursive function is a mathematical one and thus
does not concern itself with execution-specific details such as storage space or practical
execution time. No constraints are placed against the amount of memory a computer
might need to evaluate the function, the range of the arguments, or that the function
terminate before the heat death of the universe (so long as it would eventually terminate
for all inputs for which the function is defined).
The practical significance of Turing-completeness is this: there is no computation
that could be expressed in another deterministic programming language that cannot be
expressed in F♭.3 In fact, F♭ is even Turing-complete without numbers or booleans. This
language, one with only functions and application, is known as the pure lambda-calculus
and is discussed briefly in Section 2.4.4. No deterministic programming language can
compute more than the partial recursive functions.
2.3.1 F♭ Syntax
We will take the same approach in defining F♭ as we did in defining the boolean language
above. We start by describing the grammar of the F♭ language to define its concrete
syntax; the abstract syntax is deferred until Section 2.3.7. We can define the grammar
of F♭ using the following BNF:
3
This does not guarantee that the F♭ representation will be pleasant. Programs written in F♭ to
perform even fairly simplistic computations such as determining if one number is less than another are
excruciating, as we will see shortly.
CHAPTER 2. OPERATIONAL SEMANTICS 16
(Fun x -> x + 1) 2
will compute by substituting 2 for x in the function’s body x + 1, i.e. by computing 2 + 1.
This is not a very efficient method of computing, but it is a very simple and accurate
description method, and that is what operational semantics is all about – describing
clearly and unambiguously how programs are to compute.
should not evaluate to Fun x -> 3 since the inner x is bound by the inner parameter.
To correctly formalize this notion, we need to make the following definitions.
Fun x -> e
are bound, that is, any free occurrences of x in e are bound occurrences in this expression.
Similarly, in the expression
Let Rec f x = e1 In e2
occurrences of f and x are bound in e1 and occurrences of f are bound in e2 . Note that
x is not bound in e2 , but only in e1 , the body of the function.
Fun x -> x + 1
Example 2.8.
x, y, and z are all bound in the body of this function. x and y are bound by
their respective function declarations, and z is bound by the Let statement. Note
that, while F♭ contains Let as syntax, it can be defined as a macro (see Section 2.3.4
below). Binding rules work similarly for Funs and Let statements.
Example 2.9.
x is bound in the body of this function. Note that both x usages are bound
to the inner variable x.
Definition 2.9 (Closed Expression). An expression e is closed if it contains no free
variable occurrences. All programs we execute are closed (no link-time errors) – non-
closed programs don’t diverge, we can’t even contemplate executing them because they are
not in the domain of the evaluation relation.
Of the examples above, Examples 2.6, 2.8, and 2.9 are closed expressions. Example
2.7 is not a closed expression.
Now that we have an understanding of bound and free variables, we can give a formal
definition of variable substitution.
Definition 2.10 (Variable Substitution). Given a closed expression e′ , the variable sub-
stitution of x for e′ in e, denoted e[e′ /x], is the expression resulting from the operation
of replacing all free occurrences of x in e with e′ .
CHAPTER 2. OPERATIONAL SEMANTICS 19
n[v/x] = n for n ∈ Z
True[v/x] = True
False[v/x] = False
x[v/x] = v
x′ [v/x] = x′ x ̸= x′
(Fun x → e)[v/x] = (Fun x → e)
(Fun x′ → e)[v/x] = (Fun x′ → e[v/x]) x ̸= x′
(Let x = e1 In e2 )[v/x] = Let x = e1 [v/x] In e2
(Let x′ = e1 In e2 )[v/x] = Let x′ = e1 [v/x] In e2 [v/x] x ̸= x′
(e1 + e2 )[v/x] = e1 [v/x] + e2 [v/x]
(e1 − e2 )[v/x] = e1 [v/x] + e2 [v/x]
(e1 = e2 )[v/x] = e1 [v/x] = e2 [v/x]
(Not (e))[v/x] = Not (e[v/x])
(e1 And e2 )[v/x] = e1 [v/x] And e2 [v/x]
(e1 Or e2 )[v/x] = e1 [v/x] Or e2 [v/x]
(e1 e2 )[v/x] = e1 [v/x] e2 [v/x]
(If e1 Then e2 Else e3 )[v/x] = If e1 [v/x] Then e2 [v/x] Else e3 [v/x]
(Let Rec f x′ = e1 In e2 )[v/x] = Let Rec f x′ = e1 [v/x] In e2 [v/x] x ̸= x′ ̸= f
(Let Rec x x′ = e1 In e2 )[v/x] = Let Rec x x′ = e1 In e2
(Let Rec f x = e1 In e2 )[v/x] = Let Rec f x = e1 In e2 [v/x] x ̸= f
Expression:
(Fun x -> Fun y -> (x + x + y)) 5
Substitution:
(Fun y -> (x + x + y))[5/x]
= (Fun y -> (x + x + y)[5/x])
= Fun y -> (x[5/x] + x[5/x] + y[5/x])
= Fun y -> (5 + 5 + y)
α-conversion
In Example 2.9, we saw that it is possible for two variables to share the same name.
The variables themselves are, of course, distinct and follow the same rules of scope in
F♭ as they do in OCaml. But reading expressions which make frequent use of the same
CHAPTER 2. OPERATIONAL SEMANTICS 20
variable name for different variables can be very disorienting. For example, consider the
following expression.
Let Rec f x =
If x = 1 Then
(Fun f -> f (x - 1)) (Fun x -> x)
Else
f (x - 1)
In f 100
How does this expression evaluate? It is a bit difficult to tell simply by looking at it
because of the tricky bindings. We can make it much easier to understand by using
different names. α-conversion is the process of replacing a variable definition and all
occurrences bound to it with a variable of a different name.
Example 2.11.
Fun x -> x + 1
becomes
Fun z -> z + 1
Example 2.11 shows a simple case in which x is substituted for z. For cases in which
the same variable name is used numerous times, we can use the same approach. Consider
Example 2.12 in which the inner variable x is α-converted to z.
Example 2.12.
Fun x -> Fun x -> x
becomes
Fun x -> Fun z -> z
Similarly, we could rename the outer variable to z as shown in Example 2.13. Note
that in this case, the occurrence of x is not changed, as it is bound by the inner variable
and not the outer one.
Example 2.13.
Fun x -> Fun x -> x
becomes
Fun z -> Fun x -> x
Let’s figure out what variable occurrences are bound to which function in our previous
confusing function and rewrite the function in a clearer way by using α-conversion. One
possible result is as follows:
Let Rec f x =
If x = 1 Then
(Fun z -> z (x - 1)) (Fun y -> y)
Else
f (x - 1)
In f 100
Now it’s much easier to understand what is happening. If the function f is applied
to an integer which is not 1, it merely applies itself again to the argument which is one
CHAPTER 2. OPERATIONAL SEMANTICS 21
less than the one it received. Since we are evaluating f 100, that results in f 99, which
in turn evaluates f 98, and so on. Eventually, f 1 is evaluted.
When f 1 is evaluated, we explore the other branch of the If expression. We know
that x is 1 at this point, so we can see that the evaluated expression is (Fun z -> z 0)
(Fun y -> y). Using substitution gives us (Fun y -> y) 0, which in turn gives us 0.
So we can conclude that the expression above will evaluate to 0.
Observe, however, that we did not formally prove this; so far, we have been treating
substitution and other operations in a cavalier fashion. In order to create a formal proof,
we need a set of operational semantics which dictates how evaluation works in F♭. Section
2.3.3 walks through the process of creating an operational semantics for the F♭ language
and gives us the tools needed to prove what we concluded above.
(Value Rule)
v⇒v
We can also define boolean operations for F♭ in the same way as we did for the boolean
language above. Note, however, that not all values in F♭ are booleans. Fortunately, our
definition of the rules addresses this for us, as there is (for example) no logical and of
the values 5 and 3. That is, we know that these rule only apply to F♭ boolean values
because they use operations which are only defined for F♭ boolean values.
e⇒v
(Not Rule)
Not e ⇒ the negation of v
e1 ⇒ v1 e2 ⇒ v2
(And Rule)
e1 And e2 ⇒ the logical and of v1 and v2
..
.
We can also define operations over integers in much the same way. For sake of clarity,
we will explicitly restrict these rules such that they operate only on expressions which
evaluate to integers.
e1 ⇒ v1 e2 ⇒ v2 where v1 , v2 ∈ Z
(+ Rule)
e1 + e2 ⇒ the integer sum of v1 and v2
e1 ⇒ v1 e2 ⇒ v2 where v1 , v2 ∈ Z
(- Rule)
e1 - e2 ⇒ the integer difference of v1 and v2
As with the boolean rules, observe that these rules allow the ⇒ relation to be ap-
plied recursively: 5 + (4 - 3) can be evaluated using the + rule because 4 - 3 can be
evaluated using the - rule first.
CHAPTER 2. OPERATIONAL SEMANTICS 22
e1 ⇒ v1 e2 ⇒ v2 where v1 , v2 ∈ Z
(= Rule)
e1 = e2 ⇒ True if v1 and v2 are identical, else False
Note that the = rule is defined only where v1 and v2 are integers. Due to this
constraint, the expression True = True is not evaluatable in F♭. This is, of course, a
matter of choice; as a language designer, one may choose to remove that constraint and
allow boolean values to be compared directly. To formalize this, however, a change to
the rules would be required. A faithful implementation of F♭ using the above = rule is
required to reject the expression True = True.
An intuitive definition of a conditional expression is that it evalutes to the value of
one expression if the boolean is true and the value of the other expression if the boolean
is false. While this is true, the particulars of how this is expressed in the rule are vital.
Let us consider the following flawed attempt at a conditional expression rule:
e1 ⇒ v1 e2 ⇒ v2 e3 ⇒ v3
(Flawed If Rule)
If e1 Then e2 Else e3 ⇒ v2 if v1 is True, v3 otherwise
It seems that this rule allows us to evaluate many of the conditional expressions we
want to evaluate. But let us consider this expression:
If we attempted to apply the above rule to the previous expression, we would find that
the precondition e3 ⇒ v3 would not hold; there is no rule which can relate True + True ⇒
v for any v since the + rule only applies to integers. Nonetheless, we want the expression
to evaluate to 0. So how can we achieve this result?
In this case, we have no choice but to write two different rules with distinct pre-
conditions. We can capture all of the relationships in the previous rule and yet allow
expressions such as the previous to evaluate by using the following two rules:
e1 ⇒ True, e2 ⇒ v2
(If True Rule)
If e1 Then e2 Else e3 ⇒ v2
e1 ⇒ False, e3 ⇒ v3
(If False Rule)
If e1 Then e2 Else e3 ⇒ v3
Again, the key difference between these two rules is that they have different sets of
preconditions. Note that the If True rule does not evaluate e3 , nor does the If False
rule evaluate e2 . This allows the untraveled logic paths to contain unevaluatable expres-
sions without necessarily preventing the expression containing them from evaluating.
CHAPTER 2. OPERATIONAL SEMANTICS 23
Application
We are now ready to approach one of the most difficult F♭ rules: application. How can we
formalize the evaluation of an expression like (Fun x -> x + 1) (5 + 2)? We saw in
Section 2.3.2 that we can evaluate a function application by using variable substitution.
As we have a mathematical definition for the substitution operation, we can base our
function application rule around it.
Suppose we wish to evaluate the above expression. We can view application in two
parts: the function being applied and the argument to the function. We wish to know to
what the expression evaluates; thus, we are trying to establish that e1 e2 ⇒ v for some v.
?
(Application Rule (Part 1))
e1 e2 ⇒ v
e1 ⇒ v1 e2 ⇒ v2 ?
(Application Rule (Part 2))
e1 e2 ⇒ v
We obviously aren’t finished, though, as we still don’t have any preconditions which
allow us to relate v to something. Additionally, we know we will need to use variable
substitution, but we have no metavariables representing F♭ variables in the above rule.
We can fix this by reconsidering how we evaluate the first argument; we know that the
application rule only works when applying functions. In restricting our rule to applying
functions, we can name some metavariables to describe the function’s contents.
e1 ⇒ Fun x -> e e2 ⇒ v2 ?
(Application Rule (Part 3))
e1 e2 ⇒ v
In the above rule, x is the metavariable representing the function’s variable while
e represents the function’s body. Now that we have this information, we can define
function application in terms of variable substitution. When we apply Fun x -> x + 1
to a value such as 7, we wish to replace all instances of x, the function’s variable, in the
function’s body with 7, the provided argument. Formally,
F♭ Recursion
We now have a very complete set of rules for the F♭ language. We do not, however,
have a rule for Let Rec. As we will see, Let Rec is not actually necessary in basic F♭;
it is possible to build recursion out of the rules we already have. Later, however, we will
create variants of F♭ with type systems in which it will be impossible for that approach
to recursion to work. For that reason as well as our immediate convenience, we will
define the Let Rec rule now.
Again, we start with an iterative approach. We know that we want to be able to eval-
uate the Let Rec expression, so we provide metavariables to represent the components
of the expression itself.
Let Rec f x =
If x = 1 Then
1
Else
f (x - 1) + x
In f 5
If we focus on the last line (In f 5), we can see that we want the body of the
recursive function to be applied to the value 5. We can write our rule accordingly by
replacing f with the function’s body. We must make sure to use the same metavariable
to represent the function’s argument in order to allow the new function body’s variable
to be captured. We reach the following rule.
???
f (5-1) ⇒ v ′ 5 ⇒ 5
5 = 1 ⇒ False f (5-1) + 5 ⇒ v
Fun x -> · · · ⇒ Fun x -> · · · 5 ⇒ 5 If 5 = 1 Then 1 Else f (5-1) + 5 ⇒ v
(Fun x -> If x = 1 Then 1 Else f (x-1) + x) 5 ⇒ v
Let Rec f x = If x = 1 Then 1 Else f (x-1) + x In f 5 ⇒ v
As foreshadowed by its label above, our recursion rule is not yet complete. When we
reach the evaluation of f (5-1), we are at a loss; f is not bound. Without a binding for
f, we have no way of repeating the recursion.
CHAPTER 2. OPERATIONAL SEMANTICS 25
While this makes a certain measure of sense, it isn’t quite correct. In Section 2.3.2,
we saw that substitution must replace a variable with a value, while the Let Rec term
above is an expression. Fortunately, we have functions as values; thus, we can put the
expression inside of a function and ensure that we call it with the appropriate argument.
Let us consider a few examples of proof trees using the F♭ operational semantics.
Example 2.14.
Expression:
If 3 = 4 Then 5 Else 4 + 2
Proof:
3⇒3 4⇒4 4⇒4 2⇒2
3 = 4 ⇒ False 4 + 2⇒6
If 3 = 4 Then 5 Else 4 + 2 ⇒ 6
CHAPTER 2. OPERATIONAL SEMANTICS 26
(Value Rule)
v⇒v
e⇒v
(Not Rule)
Not e ⇒ the negation of v
e1 ⇒ v1 e2 ⇒ v2
(And Rule)
e1 And e2 ⇒ the logical and of v1 and v2
e1 ⇒ v1 e2 ⇒ v2
(Or Rule)
e1 Or e2 ⇒ the logical or of v1 and v2
e1 ⇒ v1 , e2 ⇒ v2 where v1 , v2 ∈ Z
(+ Rule)
e1 + e2 ⇒ the integer sum of v1 and v2
e1 ⇒ v1 , e2 ⇒ v2 where v1 , v2 ∈ Z
(- Rule)
e1 - e2 ⇒ the integer difference of v1 and v2
e1 ⇒ v1 , e2 ⇒ v2 where v1 , v2 ∈ Z
(= Rule)
e1 = e2 ⇒ True if v1 and v2 are identical, else False
e1 ⇒ True, e2 ⇒ v2
(If True Rule)
If e1 Then e2 Else e3 ⇒ v2
e1 ⇒ False, e3 ⇒ v3
(If False Rule)
If e1 Then e2 Else e3 ⇒ v3
e1 ⇒ Fun x -> e, e2 ⇒ v2 , e[v2 /x] ⇒ v
(Application Rule)
e1 e2 ⇒ v
e1 ⇒ v1 e2 [v1 /x] ⇒ v2
(Let Rule)
Let x = e1 In e2 ⇒ v2
Example 2.15.
Expression:
(Fun x -> If 3 = x Then 5 Else x + 2) 4
Proof:
by Example 2.14
Fun x -> · · · ⇒ Fun x -> · · · 4 ⇒ 4 If 3 = 4 Then 5 Else 4 + 2 ⇒ 6
(Fun x -> If 3 = x Then 5 Else x + 2) 4 ⇒ 6
Example 2.16.
Expression:
(Fun f -> Fun x -> f(f x))(Fun y -> y - 1) 4
Proof:
Due to the size of the proof, it is broken into multiple parts. We use v ⇒ ⋆ as an abbreviation
for v ⇒ v (when v is lengthy) for brevity.
Part 1:
Fun f -> Fun x -> f(f x) ⇒ ⋆ Fun y -> y - 1 ⇒ ⋆ (Fun x -> (Fun y -> y - 1) ((Fun y -> y - 1) x)) ⇒ ⋆
(Fun f -> Fun x -> f(f x))(Fun y -> y - 1) ⇒ (Fun x -> (Fun y -> y - 1) ((Fun y -> y - 1) x))
Part 2:
4⇒4 1⇒1
Fun y -> y - 1 ⇒ ⋆ 4 ⇒ 4 4 - 1⇒3 3⇒3 1⇒1
Fun y -> y - 1 ⇒ ⋆ ((Fun y -> y - 1) 4) ⇒ 3 3 - 1⇒2
(by part 1) 4⇒4 (Fun y -> y - 1) ((Fun y -> y - 1) 4) ⇒ 2
(Fun f -> Fun x -> f(f(x)))(Fun y -> y - 1) 4 ⇒ 2
Interact with F♭. Tracing through recursive evaluations is difficult, and there-
fore the reader should invest some time in exploring the semantics of Let Rec.
A good way to do this is by using the F♭ interpreter. Try evaluating the expression we
looked at above:
# Let Rec f x =
If x = 1 Then 1 Else x + f (x - 1)
In f 3;;
==> 6
# Let Rec f x =
If x = 1 Then 1 Else x + f (x - 1)
In f;;
==> Fun x ->
If x = 1 Then
1
Else
x + (Let Rec f x =
CHAPTER 2. OPERATIONAL SEMANTICS 28
If x = 1 Then
1
Else
x + (f) (x - 1)
In
f) (x - 1)
Proof. By inspection of the rules, at most one rule can apply at any time.
Proof. To show that a language is not normalizing, we simply show that there is some e
such that there is no v with e ⇒ v. Let e be (Fun x -> x x)(Fun x -> x x). e ⇏ v
for any v. Thus, F♭ is not normalizing.
The expression in this proof is a very interesting one which we will examine in more
detail in Section 2.3.5. It does not evaluate to a value because each step in its evaluation
produces itself as a precondition. This is roughly analogous to trying to prove proposition
A by using A as a given.
In this case, the expression does not evaluate because it never runs out of work to do.
This is not the only kind of non-normalizing expression which appears in F♭; another
kind consists of those expressions for which no evaluation rule applies. For example, (4
3) is a simpler expression that is non-normalizing. No rule exists to evaluate (e1 e2 )
when e1 is not a function expression.
Both of these cases look like divergence as far as the operational semantics are con-
cerned. In the case of an interpreter, however, these two kinds of expressions behave
differently. The expression which never runs out of work to do typically causes an in-
terpreter to loop infinitely (as most interpreters are not clever enough to realize that
they will never finish). The expressions which attempt application to non-functions, on
the other hand, cause an interpreter to provide a clear error in the form of an excep-
tion. This is because the error here is easily detectable; the interpreter, in attempting
to evaluate these expressions, can quickly discover that none of its available rules apply
to the expression and respond by raising an exception to that effect. Nonetheless, both
are theoretically equivalent.
Let Expressions Although F♭ introduces syntax for Let, it is quite simple to encode:
def
Let x = e1 In e2 = (Fun x -> e2 ) e1
We don’t need to do this, but it is interesting to observe that an F♭ is not made more
expressive by the existence of Let; it is just a convenience for us. An F♭-like language
without Let would be just as expressive.
For example, the program
Let x = 3 + 2 In x + x
could be rewritten as
(Fun x -> x + x) (3 + 2)
and both programs would evaluate to 10.
Pairs There are many possible encodings for pairs. The one we select here makes use
of functions and booleans. Consider that there are three important operations on a pair:
creating the pair, retrieving its first element, and retrieving its second element.
Let us first consider constructing pairs. We will construct a pair using the syntax
pr e1 e2 ; for instance, pr 3 5 represents creating a pair betweeen 3 and 5. Since we have
no pairs in F♭, we will represent them using functions that are waiting for an argument
describing which element to retrieve.
CHAPTER 2. OPERATIONAL SEMANTICS 30
def
pr = Fun x -> Fun y -> Fun b -> If b Then x Else y
In this way, our pr 3 5 example becomes
(Fun x -> Fun y -> Fun b -> If b Then x Else y) 3 5
which evaluates to
(Fun b -> If b Then 3 Else 5)
Based upon this definition of pr, we can define encodings for projecting the left and
right elements of the pair:
def
left = Fun p -> p True
def
right = Fun p -> p False
The first function takes a pair (which we expect to be constructed with pr and passes
it the value True. When evaluated, this will substitute b with True when the function
is called, execute the Then branch of the encoded pair’s function body, and thus return
the left element of the pair. For instance, we can write left (pr 3 5) to mean
(Fun p -> p True)
((Fun x -> Fun y -> Fun b -> If b Then x Else y) 3 5)
which evaluates to 3.
Tuples — that is, triples, quadruples, and so on — can be encoded in a fashion
similar to pairs. We could, for instance, use an integer rather than a boolean to pick
which element to retrieve and nest If expressions to return the correct value.
Lists Like pairs, lists can be encoded in a number of ways. One way is to rely upon
our encoding of pairs above and create nested pairs to represent the different cases of a
list. As with pairs, there are a few definitions of interest:
• cons, which takes an element and adds it to the front of a list (like OCaml’s ::
operator)
• tail, which drops the first element from a non-empty list, returning the rest of the
list
To support these operations, we imagine each list as a pair between a boolean indi-
cating whether it is empty and a value describing the list’s contents. For empty lists,
this “contents” value has no meaning; we just have it so that empty lists and non-empty
lists can both be treated as pairs. For non-empty lists, the “contents” value is itself a
pair containing the head element and the rest of the list.
We can encode the above strategy as follows:
CHAPTER 2. OPERATIONAL SEMANTICS 31
def
empty = pr True 0
def
cons = Fun h -> Fun t -> pr False (pr h t)
def
isEmpty = Fun lst -> left lst
def
head = Fun lst -> left (right lst)
def
tail = Fun lst -> right (right lst)
As an example, we might write a list of three elements as cons 1 (cons 2 (cons 3 empty)).
This creates a pair where the left element is True (to signify a non-empty list) and the
right element is itself a pair. In this inner pair, the left element is 1; the right element is
the encoded list containing the next two elements. Figure 2.2 visualises this list. In that
diagram, shaded rectangles represent the data in this list; all other parts of the diagram
are the encoding. Each empty square is a pair; all other data are either tags (indicating
whether a list is empty) or dummy values.
Freezing and Thawing We can stop and re-start computation at will by freezing and
thawing.
def
Freeze e = Fun x -> e
def
Thaw e = e 0
where x is a variable not free in e. Freeze e freezes e, keeping it from being computed.
Thaw e starts up a frozen computation. As an example, consider:
Let x = Freeze (2 + 3) In (Thaw x) + (Thaw x)
CHAPTER 2. OPERATIONAL SEMANTICS 32
This expression has same value as the equivalent expression without the freeze and thaw,
but the 2 + 3 is evaluated twice. Again, in a pure functional language the only difference
is that freezing and thawing is less efficient. In a language with side-effects, if the frozen
expression causes a side-effect, then the freeze/thaw version of the function may produce
results different from those of the original function, since the frozen side-effects will be
applied as many times as they are thawed.
Logical Combinators The classic logical combinators, simple functions for recom-
bining data, can be defined in F♭ just as a collection of functions.
def
I = Fun x -> x
def
K = Fun x -> Fun y -> x
def
S = Fun x -> Fun y -> Fun z -> (x z) (y z)
def
D = Fun x -> x x
Recall from Lemma 2.2, that a corollary to the existence of this expression is that
F♭ is not normalizing. This computation is odd in some sense. (x x) is a function
being applied to itself. There is a logical paradox at the heart of this non-normalizing
computation, namely Russell’s Paradox.
Russell’s Paradox
In Frege’s set theory (circa 1900), sets were written as predicates P (x). We can view
predicates as single-argument functions which return a boolean value: true if the argu-
ment is in the set represented by the predicate and false if it is not. Testing membership
in a set is done via application of the predicate. For example, consider the predicate
Fun x -> (x = 2 Or x = 3 Or x = 5)
This predicate represents the integer set {2, 3, 5} since it will return True for any of the
elements in that set and False for all other arguments. If we were to extend F♭ to
include a native integer less-than operator, the predicate
CHAPTER 2. OPERATIONAL SEMANTICS 33
e ∈ S iff P e ⇒ True
Russell discovered a paradox in Frege’s set theory, and it can be expressed in the
following way.
Definition 2.12 (Russell’s Paradox). Let P be the set of all sets that do not contain
themselves as members. Is P a member of P ?
Asking whether or not a set is a member of itself seems like strange question, but
in fact there are many sets that are members of themselves. The infinitely receding
set {{{{. . .}}}} has itself as a member. The set of things that are not apples is also a
member of itself (clearly, a set of non-apples is not an apple). These kinds of sets arise
only in “non-well-founded” set theory.
To explore the nature of Russell’s Paradox, let us try to answer the question it poses:
Does P contain itself as a member? Suppose the answer is yes, and P does contain itself
as a member. If that were the case then P should not be in P , which is the set of all
sets that do not contain themselves as members. Suppose, then, that the answer is no,
and that P does not contain itself as a member. Then P should have been included in
P , since it doesn’t contain itself. In other words, P is a member of P if and only if it
isn’t. Hence Russell’s Paradox is indeed a paradox.
This can also be illustrated by using F♭ functions as predicates. Specifically, we will
write a predicate for P above. We must define P to accept an argument (which we know
to be a set - a predicate in our model) and determine if it contains itself (pass it to itself
as an argument). Thus, our representation of P is Fun x -> Not(x x). We merely
apply P to itself to get our answer.
Definition 2.13 (Computational Russell’s Paradox). Let
def
P = Fun x -> Not(x x).
What is the result of P P ? Namely, what is
..
.
Not (Not ((Fun x -> Not (x x))(Fun x -> Not (x x))))
Not ((Fun x -> Not (x x)) (Fun x -> Not (x x)))
(Fun x -> Not (x x)) (Fun x -> Not (x x))
CHAPTER 2. OPERATIONAL SEMANTICS 34
We know that Not (Not (e)) evaluates to the same value as e. 5 We can see that
we’re going in circles. Again, this statement tells us that P P ⇒ True if and only if
P P ⇒ False.
This is not how Russell viewed his paradox, but it has the same core structure; it
is simply rephrased in terms of computation, and not set theory. The computational
realization of the paradox is that the predicate doesn’t compute to true or false, so its
not a sensible logical statement. Russell’s discovery of this paradox in Frege’s set theory
shook the foundations of mathematics. To solve this problem, Russell developed his
ramified theory of types, which is the ancestor of types in programming languages. The
program
is not typeable in OCaml for the same reason the corresponding predicate is not typeable
in Russell’s ramified theory of types. Try typing the above code into the OCaml top-level
and see what happens.
More information on Russell’s Paradox may be found in [14].
def
summate0 = Fun this -> Fun arg ->
If arg = 0 Then 0 Else arg + this this (arg - 1)
Note the use of this this (arg - 1). The first use of this names the function to be
applied; the second use of this is one of the arguments to that function. The argument
this allows the recursive call to invoke the function again, thus allowing us to recurse
as much as we need.
We can now sum the integers {0, 1, . . . , 7} with the expression
5
In this case, anyway, but not in general. General assertions about the equivalence of expressions
are hard to prove. In Section 2.4, we will explore a formal means of determining if two expressions are
equivalent.
CHAPTER 2. OPERATIONAL SEMANTICS 35
summate0 summate0 7
summate0 always expects its first argument this to be itself. It can then use one copy
for the recursive call (the first this) and pass the other copy on for future duplication.
So summate0 summate0 “primes the pump”, so to speak, by giving the process an initial
extra copy of itself.
Better yet, recall that currying allows us to obtain the inner function without applying
it. In essence, a function with multiple arguments could be partially evaluated, with some
arguments fixed and others waiting for input. We can use this to our advantage to define
the summate function we want:
def
summate = summate0 summate0
This allows us to hide the self-passing from the individual using our summate function,
which cleans things up considerably. We can summarize the entire process as follows,
recalling that even Let itself could be a macro for a function call:
def
summate = Let summ = Fun this -> Fun arg ->
If arg = 0 Then 0 Else arg + this this (arg - 1)
In summ summ
We now have a model for defining recursive functions without the use of the Let
Rec operator. This means that untyped languages with no built-in recursion can still
be Turing-complete. While this is an accomplishment, we can do even better; we can
abstract the idea of self-passing almost entirely out of the body of the function itself.
def
almostY = Fun body -> body body
def
summate = almostY (Fun this -> Fun arg ->
If arg = 0 Then 0 Else arg + this this (arg - 1))
The true Y -combinator actually goes one step further and allows us to write recursive
calls in the more natural style of just “this (arg - 1)”, avoiding the extra this pa-
rameter. To do this, we assume that the body argument is already in this simple form.
We then define a new form, wrapper, which replaces this with (this this) in body:
CHAPTER 2. OPERATIONAL SEMANTICS 36
def
summate = combY (Fun this -> Fun arg ->
If arg = 0 Then 0 Else arg + this (arg - 1))
The first step to making Mr. Bad do some good is rather than making an unbounded
number of Nots, Not (Not ( Not( ...))), lets make an unbounded number of some
predefined function F:
Well, that makes unbounded F (F ( F( ...))), but this sequence never terminates
and so it doesn’t do us any good. The next step is to freeze the computation at certain
points to directly stop it from continuing; recall from our freeze macro above all we need
to do is wrap some expression in a Fun -> ... 6 to freeze it:
def
makeFroFs =
(Fun x -> F (Fun -> x x)) (Fun x -> F (Fun -> x x))
and since the F we defined throws away its argument this computation would in turn
terminate with value True.
This particular example terminated too well, it threw away all the copies of F we
painstakingly made. The way we can get recursion is to make an F which sometimes uses
its argument Fun -> makeFroFs to make recursive calls by thawing it. Consider the
following revised F, aiming to ultimately be a function that sums the integers {0, 1, . . . , n}
for argument n:
def
F = Fun froFs -> Fun n ->
If n = 0 Then 0 Else n + froFs 0 (n - 1)
If makeFroFs were to use this F, it would again compute to F (Fun -> makeFroFs),
but the argument Fun -> makeFroFs is not thrown out by this new F. Consider the
computation of makeFroFs 5, by the above it is equivalent to computing
and so for the above definition of F we see parameter froFs will be instantiated with Fun
-> makeFroFs, and parameter n with 5. Because 5 = 0 is False we then compute the
else clause which after the above instantiation is
And, to compute the right-hand side of the addition, first the 0 dummy argument is
applied to un-freeze makeFroFs, so we are now setting to compute makeFroFs (5-1).
Look above – this is nearly the exact spot we started at except the argument is one
smaller! So, makeFroFs is now a recursive summation function and this example will
ultimately compute to 15. We have succeeded in repurposing the paradoxical combinator
to write recursive functions.
There are a few minor clean-up steps we can perform on the above. First, since the F
we care about are curried functions of two arguments (we need the additional argument
e.g. n for the recursive call at a different value), we can make a revised freezer Fun n
-> F n which doesn’t need to use 0 as the thawer but can “pun” and use the argument
itself to do the thawing. So, we can redo the above definitions as
def
makeFs = (Fun x -> F’ (Fun n -> (x x) n))
(Fun x -> F’ (Fun n -> (x x) n))
def
F’ = Fun fs -> Fun n ->
If n = 0 Then 0 Else n + fs (n - 1)
– we can remove the 0 argument from the recursive call here since the n-1 argument is
doing the unfreezing work via our pun.
CHAPTER 2. OPERATIONAL SEMANTICS 38
def
Y = Fun f ->
(Fun x -> f (Fun n -> (x x) n))
(Fun x -> f (Fun n -> (x x) n))
and we can apply to some concrete F, e.g. Y F’, to create our recursive summing
function. We call the above expression Y because this recursive function creator was
discovered by logicians many years ago and given that name.
Freezing and thawing, defined in Section 2.3.4, is a way to get call-by-name behavior
in a call-by-value language. Consider, then, the computation of
(3 - 2) is not evaluated until we are inside the body of the function where it is thawed,
and it is then evaluated two separate times. This is precisely the behavior of call-by-
name parameter passing, so Freeze and Thaw can encode it by this means. The fact that
(3 - 2) is executed twice shows the main weakness of call by name, namely repeated
evaluation of the function argument.
Lazy or call-by-need evaluation is a version of call-by-name that caches evaluated
function arguments the first time they are evaluated so it doesn’t have to re-evaluate them
in subsequent uses. Haskell [13, 7] is a pure functional language with lazy evaluation.
manipulation; this representation is termed its abstract syntax. To define the abstract
syntax of F♭ for a OCaml interpreter, we need to define a variant type that captures the
expressiveness of F♭. The variant types we will use are as follows.
type expr =
Var of ident | Function of ident * expr | Appl of expr * expr |
Let of ident * expr * expr | LetRec of ident * ident * expr * expr |
Plus of expr * expr | Minus of expr * expr | Equal of expr * expr |
And of expr * expr| Or of expr * expr | Not of expr |
If of expr * expr * expr | Int of int | Bool of bool
One important point here is the existence of the ident type. Notice where ident is
used in the expr type: as variable identifiers, and as function parameters for Fun and Let
Rec. The ident type attaches additional semantic information to a string, indicating
that the string specifically represents an identifier.
Note, though, that ident is used in two different ways: to signify the declaration of
a variable (such as in Function (Ident "x",...)) and to signify the use of a variable
(such as in Var(Ident "x")). Observe that the use of a variable is an expression and so
can appear in the AST anywhere that any expression can appear. The declaration of a
variable, on the other hand, is not an expression; variables are only declared in functions.
In this way, we are able to use the OCaml type system to help us keep the properties of
the AST nodes straight.
For example, consider the following AST:
Plus
Ident Int
"x" 5
At first glance, it might appear that this AST represents the F♭ expression x + 5.
However, the AST above cannot actually exist using the variants we defined above. The
Plus variation accepts two expressions upon construction and Ident is not a variant;
thus, the equivalent OCaml code Plus(Ident "x",Int 5) would not even typecheck.
The F♭ expression x + 5 is represented instead by the AST
CHAPTER 2. OPERATIONAL SEMANTICS 40
Plus
Var Int
Ident 5
"x"
Concrete: Plus
1 + 2
Int Int
Abstract:
Plus(Int 1,Int 2) 1 2
Example 2.18.
Concrete: Or
True Or False
Bool Bool
Abstract:
Or(Bool true,Bool false) true false
Example 2.19.
If
Concrete:
Not Int Int
If Not(1 = 2) Then 3 Else 4
Equal 3 4
Abstract:
If(Not(Equal(Int 1, Int 2)),
Int 3, Int 4) Int Int
1 2
CHAPTER 2. OPERATIONAL SEMANTICS 41
Example 2.20.
Appl
Concrete:
(Fun x -> x + 1) 5
Function Int
Abstract:
Ident Plus 5
Appl(
Function(
Ident "x", "x" Var Int
Plus(Var(Ident "x"),
Int 1)), Ident 1
Int 5)
"x"
Example 2.21.
Appl
Concrete:
(Fun x -> Fun y ->
x + y) 4 5 Appl Int
Example 2.22.
Concrete:
Let Rec fib x =
If x = 1 Or x = 2 Then 1 Else fib (x - 1) + fib (x - 2)
In fib 6
Abstract:
Letrec(Ident "fib", Ident "x", If(Or(Equal(Var(Ident "x"), Int 1),
Equal(Var(Ident "x"), Int 2)), Int 1, Plus(Appl(Var(Ident "fib"),
Minus(Var(Ident "x"), Int 1)), Appl(Var(Ident "fib"), Minus(Var(Ident
"x"), Int 2)))), Appl(Var(Ident "fib"), Int 6))
Letrec
If "fib" 6
Or Int
Ident 1 Ident 2
"x" "x"
Notice how lengthy even simple expressions can become when represented in the
abstract syntax. Review the above examples carefully, and try some additional examples
of your own. It is important to be able to comfortably switch between abstract and
concrete syntax when writing compilers and interpreters.
CHAPTER 2. OPERATIONAL SEMANTICS 43
Fun x -> e ∼
=
Fun z -> (Fun x -> e) z, for z not free in e
η-conversion is similar to the proxy pattern in object oriented programming[12]. A closely
related law for our freeze/thaw syntax is
Thaw (Freeze e) ∼
=e
In both examples, one of the expressions may be replaced by the other without ill effects
(besides perhaps changing execution time), so we say they are equivalent. To write
formal proofs, however, we will need to develop a more rigorous definition of equivalence.
We wish to study equivalence for possibly open programs, because there are good equiva-
lences such as x + 1 - 1 ∼ = x + 0. We define “at any place” by the notion of a program
context, which is, informally, a F♭ program with some holes (•) in it. Using this infor-
mal definition, testing if e1 ∼
= e2 would be roughly equivalent to performing the following
steps (for all possible programs and all possible holes, of course).
5. Repeat steps 1-4 for every possible context. If none of these infinitely many contexts
produces different results, then e1 is equivalent to e2 .
Now let us elaborate on the notion of a program context. Take an F♭ program with
some “holes” (•) punched in it: replace some subterms of any expression with •. Then
“hole-filling” in this program context C, written C[e], means replacing • with e in C.
Hole filling is like substitution, but without the concerns of bound or free variables. It
is direct replacement with no conditions.
Let us look at an example of contexts and hole-filling using η-conversion as we defined
above. Let
def
C = (Fun z -> Fun x -> • ) z
mathematical objects (complete and continuous partial orders). We are not going to
discuss domain theory here, mainly because it does not generalize well to programming
languages with side effects. [16] explores the relationship between operational semantics
and domain theory.
e∼
=e
e∼
= e′ if e′ ∼
=e
e∼
= e′′ if e ∼
= e′ and e′ ∼
= e′′
C[e] ∼
= C[e′ ] if e ∼
= e′
((Fun x -> e) v) ∼
= (e[v/x])
provided no free variables in v become bound variables in e[v/x] (i.e. no free variables are
captured). Note that v being closed is one condition that ensures that no free variables
will be captured.
Definition 2.22 (η-Equivalence).
(Fun x -> e) ∼
= (Fun z -> (Fun x -> e) z)
(Fun x -> e) ∼
= (Fun y -> (e[y/x]))
Definition 2.24.
(n + n′ ) ∼
= the sum of n and n′
Similar equations hold for -, And, Or, Not, and = applied to concrete integer or boolean
values.
Definition 2.25 (Let laws). return Let x = e In x ∼
=e
pure functional law Let x1 = e1 In Let x2 = e2 In e3 ∼ = Let x2 = e2 In Let x1 = e1 In e3
provided x1 /x2 do not occur freely in e1 or e2 .
CHAPTER 2. OPERATIONAL SEMANTICS 46
Let-α Let x = e1 In e2 ∼
= Let y = e1 In e2 [y/x]
Definition 2.26 (If laws).
Definition 2.27.
If e ⇒ v then e ∼
=v
Note that, in the above proof, we used the expression (0 0). This expression cannot
evaluate; the application rule applies only to functions. As a result, this expression makes
an excellent tool for intentionally making code get stuck when building a proof about
operational equivalence. Other similar get-stuck expressions exist, such as True + True,
Not 5, and the ever-popular (Fun x -> x x)(Fun x -> x x).
It should be clear that we can use the approach in Lemma 2.5 to prove that any
two integers which are not equal are not operationally equivalent. But can we make a
statement about a non-value expression?
Lemma 2.6. x ≇ x + 1 - 1.
At first glance, this inequivalence may seem counterintuitive. But the proof is fairly
simple:
CHAPTER 2. OPERATIONAL SEMANTICS 47
def
Proof. By example. Let C = (Fun x -> •) True. Then C[x] ⇒ True. C[x + 1 - 1] ≡
(Fun x -> x + 1 - 1) True, which cannot evaluate because no rule allows us to eval-
uate True + 1. Thus, by definition, x ≇ x + 1 - 1.
Lemma 2.7. If e ⇏ v for any v, e′ ⇏ v ′ for any v ′ , and both e and e′ are closed
expressions, then e ∼
= e′ . For example, (0 0) ∼
= (Fun x -> x x)(Fun x -> x x).
That is, If True Then 0 Else 1 touches the subexpression 0 because it is evaluated
when the whole expression is evaluated. 1 is not touched because the evaluation of the
expression never causes 1 to be evaluated.
We can now prove Lemma 2.7.
The above proof is much longer and more complex than any of the proofs of in-
equivalence that we have encountered and it isn’t even very robust. It could benefit, for
example, from a proof that a closed expression cannot be changed by a replacement rule
(which is taken for granted above). Furthermore, the above proof doesn’t even prove a
very useful fact! Of far more interest would be proving the operational equivalence of
two expressions when one of them is executed in a more desirable manner than the other
(faster, fewer resources, etc.) in order to motivate compiler optimizations.
Lemma 2.7 is, however, effective in demonstrating the complexities involved in prov-
ing operational equivalence. It is surprisingly difficult to prove that an equivalence holds;
even proving 1 + 1 ∼ = 2 is quite challenging. See [16] for more information on this topic.
CHAPTER 2. OPERATIONAL SEMANTICS 48
• Even programs with free variables can execute (or reduce in λ-calculus terminol-
ogy).
• Reduction can happen anywhere, e.g. inside a function body that hasn’t been
called yet.
• (λx.e)e′ ⇒ e[e′ /x] is the only reduction rule, called β-reduction. (It has a special
side-condition that it must be capture-free, i.e. no free variables in e′ become bound
in the result. Capture is one of the complications of allowing reduction anywhere.)
This form of computation is conceptually very interesting, but is more distant from
how actual computer languages execute and so we do not put a strong focus on it here.
Exercises
Exercise 2.1. How would you change the Sheep language to allow the terms bah, baah, · · ·
without excluding any terms which are already allowed?
Exercise 2.2. Is the term it in the Frog language? Why or why not?
Exercise 2.3. Is it possible to construct a term in Frog without using the terminal t?
If so, give an example. If not, why not?
Exercise 2.4. Complete the definition of the operational semantics for the boolean
language described in section 2.2.1 by writing the rules for Or and Implies.
Exercise 2.5. Why not just use interpreters and forget about the operational semantics
approach?
Chapter 3
In Chapter 2 we saw that, using a language with only functions and application, we
could represent advanced programming constructs such as tuples and lists. However,
we pointed out that these encodings have fundamental problems, such as a low degree
of efficiency, and the fact that they necessarily expose their details to the programmer,
making them difficult and dangerous to work with in practice. Recall how we could take
our encoding of a pair from Chapter 2 and apply it like a function; clearly the wrong
behavior. In this chapter we look at how we can build some of these advanced features
into the language, namely tuples and records, and we conclude the chapter by examining
variants.
3.1 Tuples
One of the most fundamental forms of data aggregation in programming is the notion of
pairing. With pairs, or 2-tuples, almost any data structure can be represented. Tripling
can be represented as (1, (2, 3)), and in general n-tuples can be represented with pairs
in a similar fashion. Records and C-style structs can be represented with sets (n-tuples)
of (label, value)-pairs. Even objects can be built up from pairs, but this is stretching it
(just as encoding pairs as functions was stretching it).
In Chapter 2, we showed an encoding of pairs based on functions. There were two
problems with this representation of pairs. First of all, the representation was inefficient.
More importantly, the behavior of pairs was slightly wrong, because we could apply
them like functions. To really handle pairs correctly, we need to add them directly to
the language. We can add pairs to F♭ in a fairly straightforward manner, making a new
language F♭P.
49
CHAPTER 3. TUPLES, RECORDS, AND VARIANTS 50
x ::= . . . variables
v ::= . . . | (v, v) values
e ::= . . . | (e, e) | Left e | Right e expressions
We want our pairs to be eager, that is, the left and right components of the pair are
evaluated, and must be values for the pair itself to be considered a value. For example,
we want (2,3+4) ⇒ (2,7) in our operational semantics. OCaml tuples exhibit this same
behavior. Also notice that our space of values is now bigger, pairs above are values as
long as both components of the pair are themselves values. So, (2,3+4) is not a value,
but (2,7) is.
We define the operational semantics of F♭P as an extension of the operational se-
mantics for F♭ given in Figure 2.1, by adding the following additional rules:
e1 ⇒ v1 e2 ⇒ v2
(Pair)
(e1 , e2 ) ⇒ (v1 , v2 )
e ⇒ (v1 , v2 )
(Left)
Left e ⇒ v1
e ⇒ (v1 , v2 )
(Right)
Right e ⇒ v2
type expr =
...
| Pr of expr * expr | Left of expr | Right of expr
Exercise 3.1. How would we write our interpreter to handle pairs in a non-eager way?
In other words, what would need to be in the interpreter so that (e1 , e2 ) was considered
a value (as opposed to only (v1 , v2 ) being considered a value)?
Now that we have 2-tuples, encoding 3-tuples, 4-tuples, and n-tuples is easy. We
simply express e.g. (1, 2, 3, ..., ..., n) as (1, (2, (3, (..., n)))). As we
saw before, lists can in turn be encoded as n-tuples.
3.2 Records
Records are a variation on tuples in which the fields have names. Records have several
advantages over tuples. The main advantage is the named field. From a software engi-
neering perspective, a named field “zipcode” is far superior to “the third element in the
tuple.” The order of the fields of a record is arbitrary, unlike with tuples.
Records are also far closer to objects than tuples are. We can encode object poly-
morphism via record polymorphism. Record polymorphism is discussed in Section 3.2.1.
The motivation for using records to encode objects is that a subclass is composed of a
superset of the fields of its parent class, and yet instances of both classes may be used in
the context of the superclass. Similarly, record polymorphism allows records to be used
in the context of a subset of their fields, and so the mapping is quite natural. We will
use records to model objects in Chapter 5.
Our F♭ records will have the same syntax as OCaml records. That is, records are
written as { ℓ1 = e1 ; ℓ2 = e2 ; . . . ; ℓn = en }, and selection is written as e.ℓk , which
selects the value labeled ℓk from record e. We use ℓ as a metavariable ranging over
labels, just as we use e as a metavariable indicating an expression; an actual record is
for instance { x = 5; y = 7; z = 6 }, so x here is an actual label.
If records are always statically known to be of fixed size, that is, if they are known
to be of fixed size at the time we write our code, then we may simply map the labels to
integers, and encode the record as a tuple. For instance,
Obviously, the makes for ugly, hard-to-read code, but for C-style structs, it works.
But in the case where records can shrink and grow, this encoding is fundamentally too
weak. C++ structs can be subtypes of one another, so fields that are not declared may,
in fact, be present at runtime.
On the other hand, pairs can be encoded as records quite nicely. The pair (3, 4)
can simply be encoded as the record { l = 3; r = 4 }. More complex pairs, such as
those used to represent lists, can also be encoded as records. For example, the pair (3,
(4, (5, 6))), which represents the list [3; 4; 5; 6], can be encoded as the record
{ l = 3; r = { l = 4; r = { l = 5; r = 6 }}}.
A variation of this list encoding is used in the mergesort example in Section 4.3.2,
where { l = 3; r = { l = 4; r = { l = 5; r = { l = 6; r = emptylist }}}} is
CHAPTER 3. TUPLES, RECORDS, AND VARIANTS 52
the encoding for the list. This encoding has the nice property that the values are always
contained in the l fields, and the rest of the list is always contained in the r fields. This
is much closer to the way real languages such as OCaml, Scheme, and Lisp represent lists
(recall how we write statements like let (first::rest) = mylist in OCaml).
e1 ⇒ v1 . . . en ⇒ vn
(Record)
{ ℓ1 = e1 ; . . . ; ℓn = en } ⇒ { ℓ1 = v1 ; . . . ; ℓn = vn }
e ⇒ { ℓ1 = v1 ; . . . ; ℓn = vn } 1≤i≤n
(Projection)
e.ℓi ⇒ vi
Here n is the length of the record, and the . . . notation is standard mathematical
notation for expressing arbitrary sequences of length n. To be clear n = 0 and n = 1 are
both reasonable options in this notation, n = 0 being the empty record { } and n = 1
being the singleton record { ℓ1 = e1 }.
Next, we need a way to represent the record itself. Records may be of arbitrary length,
so a list of (label, expression)-pairs is needed. In addition, we need a way to represent
selection. The F♭R expr type now looks like the following.
Let’s look at some concrete to abstract syntax examples for our new language.
Example 3.1.
[Link]
Notice that our interpreter correctly handles {}, the empty record, by having it
compute to the itself since it is, by definition, a value.
Interact with F♭SR. We can use our F♭SR interpreter to explore records (F♭SR
is F♭ with records and state, and is introduced in Chapter 4). First, let’s try a
simple example to demonstrate the eager evaluation of records.
# { one = 1; two = 2;
three = 2 + 1; four = (Fun x -> x + x) 2 };;
==> { one = 1; two = 2; three = 3; four =4 }
Next, let’s try a more interesting example, where we use records to encode lists. Note
that we define emptylist as -1. The function below sums all values in a list (assuming
it has a list of integers).
# Let emptylist = 0 - 1 In
Let Rec sumlist list =
If list = emptylist Then
0
Else
(list.l) + sumlist (list.r) In
sumlist { l = 1; r = { l = 2; r = { l = 3; r = { l = 4; r = emptylist}}}};;
==> 10
CHAPTER 3. TUPLES, RECORDS, AND VARIANTS 55
3.3 Variants
We have been using variants in OCaml, as the types for expressions expr. Now we study
untyped variants more closely. OCaml actually has two (incompatible) forms of variant,
regular variants and polymorphic variants . In the untyped context we are working in,
the OCaml polymorphic variants are more appropriate and we will use that form.
We briefly contrast the two forms of variant in OCaml for readers unfamiliar with
polymorphic variants. Recall that in OCaml, regular variants are first declared as types
type feeling =
Vaguely of feeling | Mixed of feeling * feeling |
Love of string | Hate of string | Happy | Depressed
We require all variant names to be a backtick followed by a label, ‘ℓ. This follows the
notation for OCaml inferred variants which are more in the spirit of F♭V variants as we
require no type declarations. Note that ‘ℓ(v) is a value, provided v is: as with records
CHAPTER 3. TUPLES, RECORDS, AND VARIANTS 56
and pairs, variants are eager, and we need to evaluate the component before we have a
variant value.
To define the meaning of F♭V execution, we extend the operational semantics of F♭
with the following two rules:
e⇒v
(Variant Rule)
‘ℓ(e) ⇒ ‘ℓ(v)
e ⇒ ‘ℓi (vi ) ei [vi /xi ] ⇒ v 1 ≤ i ≤ n
(Match Rule)
Match e With ‘ℓ1 (x1 ) -> e1 | . . . | ‘ℓn (xn ) -> en ⇒ v
The Variant rule constructs a new variant labeled ‘ℓ; its argument is eagerly evaluated
to a value, just as in OCaml: ‘Positive(3+2) ⇒ ‘Positive(5). The Match rule first
computes the expression e being matched to a variant ‘ℓi (vi ) for 1 ≤ i ≤ n, and then
looks up that variant in the match, finding ‘ℓi (xi ) -> ei , and then evaluating ei with
its variable xi given the value of the variant argument. Here the n represents how many
cases the match has, and 1 ≤ i ≤ n is some i in the list of variants.
Example 3.3.
Exercise 3.2. Extend the F♭V syntax and operational semantics so the Match expression
always has a final match of the form of the form“| -> e”. Is this Match strictly more
expressive than the old one, or not?
The Abstract Syntax for Variants The abstract syntax for F♭V is as follows. First,
each variant needs a name.
Let’s look at some concrete to abstract syntax examples for our new language.
Example 3.4.
‘Positive(4)
Example 3.5.
Match e With
‘Positive(x) -> 1 | ‘Negative(y) -> -1 | ‘Zero(p) -> 0
Variants and Records are Duals Here we see how the definition of a record is
modeled as a use of a variant, and a use of a record is the definition of a variant.
Variants are the dual of records: a record is this field and that field and that field;
a variant is this field or that field or that field. Since they are duals, defining a record
looks something like using a variant, and defining a variant looks like using a record.
Variants can directly encode records and vice-versa, in a programming analogy of
how DeMorgan’s Laws allows logical and to be encoded in terms of or, and vice-versa:
p Or q = Not(Not p And Not q); p And q = Not(Not p Or Not q).
Variants can be encoded using records as follows.
The tricky part of the encoding is that definitions must be turned in to uses and
vice-versa. This is done with functions: an injection is modeled as a function which is
given a record and will select the specified field.
Here is how records can be encoded using variants.
We will now leave the world of pure functional programming, and begin considering
languages with side-effects. For now we will focus solely on two particular side-effects,
state and exceptions. There are, however, many other types of side-effects, including the
following.
4.1 State
Languages like F♭, F♭R, and F♭V are pure functional languages. Once we add any
kind of side-effect to a language it is not pure functional anymore. Side-effects are non-
local, meaning they can affect other parts of the program. As an example, consider the
following OCaml code.
let x = ref 9 in
let f z = x := !x + z in
x := 5; f 5; !x
This expression evaluates to 10. Even though x was defined as a reference to 9 when
f was declared, the last line sets x to be a reference to 5, and during the application of f,
x is reassigned to be a reference to (5 + 5), making it 10 when it is finally dereferenced
in the last line. Clearly, the use of side effects makes a program much more difficult
to analyze, since it is not as declarative as a functional program. When looking at
programs with side-effects, one must examine the entire body of code to see which side-
effects influence the outcome of which expressions. Therefore, it is a good programming
moral to use side-effects only when they are strongly needed.
Let us begin by informally discussing the semantics of references. In essence, when
a reference is created in a program, a cell is created that contains the specified value,
and the reference itself is a pointer to that cell. A good metaphor for these reference
58
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 59
... !c + 4 ...
... c := 1 + x ...
... c := !c + 1 ...
cells is to think of each one as a junction box. Then, each assignment is a path into
the junction, and each read, or dereference, is a path going out of the junction. The
reference cell, or junction, sits to the side of the program, and allows distant parts
of the program to communicate with one another. This “distant communication” can
be a useful programming paradigm, but again it should be used sparingly. Figure 4.1
illustrates this metaphor.
In the world of C++ and Java, non-const (or non-final) global variables are the most
notorious form of reference. While globals make it easy to do certain tasks, they generally
make programs difficult to debug, since they can be altered anywhere in the code.
x ::= ...
c ::= (nonces) cell names
v ::= ... | c cell values
e ::= . . . | Ref e | e := e′ | !e state expressions
The expression additions are just following OCaml’s notation; the unusual aspect is the
cells, c. Cell names are abstract heap locations; concretely they can be thought of as
just integers aka memory locations. We need cell names because Ref 5 performs a heap
allocation and it needs to return a reference to that heap value, a location c, in the
store. Because cell names refer to locations in the heap, and the heap is initially empty,
programs have no cell names c when the execution begins.
The most significant departure from the pure functional operational semantics we
have looked at so far is that the evaluation relation e ⇒ v alone is not sufficient to
capture the semantics of a state-based language. The evaluation of an expression can
now produce side-effects, and our ⇒ rule needs to incorporate this. Specifically, we
will need to have someplace to record all these side-effects: a store. In C, a store is a
stack and a heap, and memory locations are referenced by their addresses. When we are
writing our interpreter, we will only have access to the heap, so we will need to create
an abstract store in which to record side-effects.
⟨e, S0 ⟩ ⇒ ⟨v, S⟩
A key aspect of this mapping is that every computation state includes both an initial
store and a final store; this is because the store is a global state that needs to be threaded
through the computation. At the start of computation, S0 is initially empty, and S is
the final store when computation terminates. In the process of evaluation, cells, c, will
begin to appear in the program syntax, as references to memory locations. Cells are
values since they do not need to be evaluated; this is why cells c in the above grammar
are considered values.
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 61
Note how the store here is threaded through the different evaluations, showing how
changes in the store in one place propagate to the store in other places, and in a fixed
order that reflects the intended evaluation order. Here we start out with S1 as the initial
store, and evaluate e1 using that store. e1 may itself have side effects, which are reflected
in the resulting store, S2 . Since this is now the latest store when we next execute the
body of the Let, e2 [v1 /x], we want to start with that store. The e2 [v1 /x] evaluation also
may alter the store and returns store S3 . Since this is the absolute most recent store it
is the one we want to return below the line in the rule.
The rules for our new memory operations are as follows.
⟨e, S1 ⟩ ⇒ ⟨v, S2 ⟩
(Reference Creation)
⟨Ref e, S1 ⟩ ⇒ ⟨c, S2 {c 7→ v}⟩, for c ∈
/ Dom(S2 )
⟨e, S1 ⟩ ⇒ ⟨c, S2 ⟩
(Dereference)
⟨!e, S1 ⟩ ⇒ ⟨v, S2 ⟩, where S2 (c) = v
⟨e1 , S1 ⟩ ⇒ ⟨c, S2 ⟩ ⟨e2 , S2 ⟩ ⇒ ⟨v, S3 ⟩
(Assignment)
⟨e1 := e2 , S1 ⟩ ⇒ ⟨v, S3 {c 7→ v}⟩
All of the other F♭ syntax elements also need to have the store threaded since any
function call, conditional, etc may include store operations. For completeness here are
the remaining rules (leaving out only Let Rec).
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 62
(Value)
⟨v, S⟩ ⇒ ⟨v, S⟩
⟨e, S1 ⟩ ⇒ ⟨v, S2 ⟩
(Not)
⟨Not e, S1 ⟩ ⇒ ⟨the negation of v, S2 ⟩
⟨e1 , S1 ⟩ ⇒ ⟨v1 , S2 ⟩ ⟨e2 , S2 ⟩ ⇒ ⟨v2 , S3 ⟩
(And)
⟨e1 And e2 , S1 ⟩ ⇒ ⟨the logical and of v1 and v2 , S3 ⟩
⟨e1 , S1 ⟩ ⇒ ⟨v1 , S2 ⟩ ⟨e2 , S2 ⟩ ⇒ ⟨v2 , S3 ⟩
(Or)
⟨e1 Or e2 , S1 ⟩ ⇒ ⟨the logical or of v1 and v2 , S3 ⟩
⟨e1 , S1 ⟩ ⇒ ⟨v1 , S2 ⟩ ⟨e2 , S2 ⟩ ⇒ ⟨v2 , S3 ⟩ v1 , v2 ∈ Z
(+)
⟨e1 + e2 , S1 ⟩ ⇒ ⟨the integer sum of v1 and v2 , S3 ⟩
⟨e1 , S1 ⟩ ⇒ ⟨v1 , S2 ⟩ ⟨e2 , S2 ⟩ ⇒ ⟨v2 , S3 ⟩ v1 , v2 ∈ Z
(-)
⟨e1 - e2 , S1 ⟩ ⇒ ⟨the integer difference of v1 and v2 , S3 ⟩
⟨e1 , S1 ⟩ ⇒ ⟨v1 , S2 ⟩ ⟨e2 , S2 ⟩ ⇒ ⟨v2 , S3 ⟩ v1 , v2 ∈ Z
(=)
⟨e1 = e2 , S1 ⟩ ⇒ ⟨True if v1 and v2 are identical, else False, S3 ⟩
⟨e1 , S1 ⟩ ⇒ ⟨True, S2 ⟩ ⟨e2 , S2 ⟩ ⇒ ⟨v2 , S3 ⟩
(If True)
⟨If e1 Then e2 Else e3 , S1 ⟩ ⇒ ⟨v2 , S3 ⟩
⟨e1 , S1 ⟩ ⇒ ⟨False, S2 ⟩ ⟨e3 , S2 ⟩ ⇒ ⟨v2 , S3 ⟩
(If False)
⟨If e1 Then e2 Else e3 , S1 ⟩ ⇒ ⟨v2 , S3 ⟩
⟨e1 , S1 ⟩ ⇒ ⟨Fun x -> e, S2 ⟩ ⟨e2 , S2 ⟩ ⇒ ⟨v2 , S3 ⟩ ⟨e[v2 /x], S3 ⟩ ⇒ ⟨v, S4 ⟩
(Application)
⟨e1 e2 , S1 ⟩ ⇒ ⟨v, S4 ⟩
These rules can be tricky to evaluate because the store needs to be kept up to date
at all points in the evaluation. Let us look at a few example expressions to get a feel for
how this works.
Example 4.1.
Example 4.2.
F♭S Interpreters
Just as we had to modify the evaluation relation in our F♭S operational semantics to
support state, writing a F♭S interpreter will also require some additional work. There are
two obvious approaches to take, and we will treat them both below. The first approach
involves mimicking the operational semantics and defining evaluation on an expression
and a store together. This approach yields a functional interpreter in which eval(e, S0 )
for expression e and initial state S0 returns the tuple (v, S), where v is the resulting
value and S is the final state.
The second and more efficient design involves keeping track of state in a global, muta-
ble dictionary structure. This is generally how real implementations work. This approach
results in more familiar evaluation semantics, namely eval e returns v. Obviously such
an interpreter is no longer functional, but rather, imperative. We would like such an
interpreter to faithfully implement the operational semantics of F♭S, and thus we would
ideally want a theorem that states that this approach is equivalent to the first approach.
Proving such a theorem would be difficult, however, mainly because our proof would rely
on the operational semantics of OCaml, or whatever implementation language we chose.
We will therefore take it on good faith that the two approaches are indeed equivalent.
sig
(* ... *)
end
struct
let empty = (* initial empty store *)
let fresh = (* returns a fresh Cell name *)
let count = ref 0 in
function () -> ( count := !count + 1; Cell(!count) )
(* Note: this is not purely functional! It is quite
* difficult to make fresh purely functional.
*)
module FbSEvalFunctor =
functor (Store : STORE) ->
struct
(* ... *)
(Int (n+n’),s’’)
end
The Imperative Interpreter The stateful, imperative F♭S interpreter is more effi-
cient than its functional counterpart, because no threading is needed. In the imperative
interpreter, we represent the store as a dictionary structure (similar to Java’s HashMap
class or the C++ STL map template). The eval function needs no extra store parameter,
provided that it has a reference to the global dictionary. Non-store-related rules such as
Plus and Minus are completely ignorant of the store. Only the direct store evaluation
rules, Ref, Set, and Get actually extend, update, or query the store. A good evaluator
would also periodically garbage collect, or remove unneeded store elements. Garbage col-
lection is discussed briefly in Section 4.1.4. The implementation of the stateful interpreter
is left as an exercise to the reader.
Side-Effecting Operators
Now that we have a mutable store, our code has properties beyond the value that is
returned, namely, side-effects. Operators such as sequencing (;), and While- and For
loops now become relevant. These syntactic concepts are easily defined as macros, so we
will not add them to the official F♭S syntax. The macro definitions are as follows.
e1 ; e2 = (Fun x -> e2 ) e1
While e Do e′ = Let Rec f x = If e Then f e′ Else 0 In f 0
Exercise 4.1. Why are sequencing and loop operations irrelevant in pure functional
languages like F♭?
Cell(48)
Cell 48
Let x = Ref 0 in x := x
This is the simplest possible store cycle, where a cell directly points to itself. This type
of store is illustrated in Figure 4.2.
Exercise 4.2. In the above example, what does !!!!!!!!x return? Can a store cycle
like the one above be written in OCaml? Why or why not? (Hint: What is the type of
such an expression?)
A more subtle form of a store cycle is when a function is placed in the cell, and the
body of the function refers to that cell. Consider the following example.
Let c = Ref 0 In
c := (Fun x -> If x = 0 Then 0 Else 1 + !c(x-1));
!c(10)
Cell c contains a function, which, in turn, refers to c. In other words, the function
has a reference to itself, and can apply itself, giving us recursion. This form of recursion
is known as tying the knot, and is the method used by most compilers to implement
recursion. A similar technique is used to make objects self-aware, although C++ explicitly
passes self, and is more like the Y -combinator.
Exercise 4.3. Tying the knot can be written in OCaml, but not directly as above. How
must the reference be declared for this to work? Why can we create this sort of cyclic
store, but not the sort described in Exercise 4.2?
Interact with F♭SR. Let’s write a recursive multiplication function in F♭SR,
first using Let Rec, and then by tying the knot.
Now we’ll use tying the knot. Because F♭SR does not include a sequencing operation,
we use the encoding presented in Section 4.1.1.
type expr =
...
| Get of expr | Set of ident * expr | Ref of expr
For the variable on the left side of the assignment, we would need the address, and not
the contents, of the variable.
A final issue with the standard notion of state in C and C++ is the problem of
uninitialized variables. Because variables are not required to be initialized, runtime
errors due to these uninitialized variables are possible. Note that the Ref syntax of F♭S
and OCaml requires that the variable be explicitly initialized as part of its declaration.
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 68
When f (3) is computed, the environment binds x to 3 while the body is computed, so
the result returned is
Fun y -> x + y,
but it would be a mistake to simply return this as the correct value, because we would
lose the fact that x was bound to 3. The solution is that when a function value is
returned, the closure of that function is returned.
Definition 4.2 (Closure). A closure is a function along with an environment, such that
all free values in the function body are bound to values in the environment.
For the case above, the correct value to return is the closure
This closure view of function values is critical when writing a compiler, since com-
piler’s should not be doing substitutions of code on the fly. Compilers are discussed in
Chapter 8.
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 70
type expr =
Var of ident | Function of ident * expr | Appl of expr * expr |
LetRec of ident * ident * expr * expr | Plus of expr * expr |
Minus of expr * expr | Equal of expr * expr |
And of expr * expr | Or of expr * expr | Not of expr |
If of expr * expr * expr | Int of int | Bool of bool |
Ref of expr | Set of expr * expr | Get of expr | Cell of int |
Record of (label * expr) list | Select of label * expr |
Let of ident * expr * expr
type fbtype =
TInt | TBool | TArrow of fbtype * fbtype | TVar of string |
TRecord of (label * fbtype) list | TCell of fbtype;;
In the next two sections we will look at some nontrivial “real-world” F♭SR programs
to illustrate the power we can get from this simple language. We begin by considering
a function to calculate the factorial function, and conclude the chapter by examining an
implementation of the merge sort algorithm.
(*
* First we encode multiplication for positive nonnegative
* integers. Notice the currying in the Let Rec construct.
* Multiplication is encoded in the obvious way: repeated
* addition.
*)
Let Rec mult x = Fun y ->
If y = 0 Then
0
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 71
Else
x + (mult x (y - 1)) In
(*
* Now that we have multiplication, factorial is easy to
* define.
*)
Let Rec fact x =
If x = 0 Then
1
Else
mult x (fact (x - 1)) In
fact 7
Therefore it suffices to encode lesseq. But how can we do this using only the regular
F♭SR operators? The basic idea is as follows. To test if x is less than or equal to y we
compute a z such that x + z = y. If z is nonnegative, we know that x is less than or
equal to y. If z is negative, we know that x is greater than y.
At first glance, we seem to have a “chicken and egg” problem here. How can we tell
if z is negative if we have no comparison operator. And how do we actually compute
z to begin with? One idea would be to start with z = 0 and loop, incrementing z by
1 at every step, and it testing if x + z = y. If we find a z, we stop. We know that z
is positive, and we conclude that x is less than or equal to y. If we don’t find a z, we
start at z = −1 and perform a similar loop, decrementing z by 1 every step. If we find
a proper value of z this way, we conclude that x is greater then y.
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 72
The flaw in the above scheme should be obvious: if x > y, the first loop will never
terminate. Indeed we would need to run both loops in parallel for this idea to work!
Obviously F♭SR doesn’t allow for parallel computation, but there is a solution along
these lines. We can interleave the values of z that are tested by the two loops. That is,
we can try the values {0, −1, 1, −2, 2, −3, . . .}.
The great thing about this approach is that every other value of z is negative, and we
can simply pass along a boolean value to represent the sign of z. If z is nonnegative, the
next iteration of the loop inverts it and subtracts 1. If z is negative, the next iteration
simply inverts it. Note that we can invert the sign of an number x in F♭SR by simply
writing 0 - x. Now, armed with our ideas about lesseq, let us start writing our merge
sort.
Let cons = Fun elt -> Fun seq -> {l=elt; r=seq} In
handlers. The reader should already be familiar with the basics of exceptions from
working with the OCaml exception mechanism.
There are some other control operations that we will not discuss, such as the call/cc,
shift/reset, and control/prompt operators.
We will avoid the goto operator in this section, except for the following brief discus-
sion. goto basically jumps to a labeled position in a function. The main problem with
goto is that it is too raw. The paradigm of jumping around between labels is not all that
useful in the context of functions. It is also inherently dangerous, as one may inadver-
tently jump into the middle of a function that is not even executing, or skip past variable
initializations. In addition, with a rich enough set of other control operators, goto really
doesn’t provide any more expressiveness, at least not meaningful expressiveness.
The truth is that control operators are really not needed at all. Recall that F♭, and
the lambda calculus for that matter, are already Turing-complete. Control operators are
therefore just conveniences that make programming easier. It is useful to think of control
operators as “meta-operators,” that is, operators that act on the evaluation process itself.
(Fun x ->
(If x = 0 Then 5 Else Return (4 + x)) - 8) 4
Since x will not be 0 when the function is applied, the Return statement will get evalu-
ated, and execution should stop immediately, not evaluating the “- 8.” The problem is
that evaluating the above statement means evaluating
(Return (4 + 4)) - 8.
But we know that the subtraction rule works by evaluating the left and right hand sides
of this expression to values, and performing integer subtraction on them. Clearly that
doesn’t work in this case, and so we need a special rules for subtraction with a Return
in one of the subexpressions.
First, we need to add Returns to the value space of F♭ and provide an appropriate
evaluation rule:
e⇒v
(Return)
Return e ⇒ Return v
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 76
Next, we need the special subtraction rules, one for when the Return is on the left side,
and one for when the Return is on the right side.
e ⇒ Return v
(- Return Left)
e - e′ ⇒ Return v
e ⇒ v e′ ⇒ Return v ′
(- Return Right) v is not of the form Return v ′′
e - e′ ⇒ Return v ′
Notice that these subtraction rules evaluate to Return v and not simply v. This
means that the Return is “bubbled up” through the subtraction operator. We need to
define similar return rules for every F♭ operator. Using these new rules, it is clear that
Return (4 + 4) - 8 ⇒ Return 8.
Of course, we don’t want the Return to bubble up indefinitely. When the Return
pops out of the function application, we only get the value associated with it. In other
words, our original expression,
(Fun x ->
(If x = 0 Then 5 Else Return (4 + x)) - 8) 4
A few other special application rules are needed for the cases when either the function
or the argument itself is a Return.
e1 ⇒ Return v
(Appl. Return Fun)
e1 e2 ⇒ Return v
e1 ⇒ v1 , e2 ⇒ Return v
(Appl. Return Arg.)
e1 e2 ⇒ Return v
Of course, we still need the original function application rule (see Section 2.3.3) for the
case that function execution implicitly returns a value by dropping off the end of its
execution.
Let us conclude our discussion of Return by considering the effect of Return Return e.
There are two possible interpretations for such an expression. By the above rules, this
expression returns from two levels of function calls. Another interpretation would be to
add the following rule:
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 77
e ⇒ Return v
(Double Return)
Return e ⇒ Return v
Of course we would need to restrict the original Return rule to the case where v was not
in the form Return v. With this rule, instead of returning from two levels of function
calls, the second Return actually interrupts and bubbles through the first. Of course,
double returns are not a common construct, and these rules will not be used often in
practice.
(function x ->
try
(if x = 0 then 5 else raise (Return (4 + x))) - 8
with
Return n -> n) 4
Now, let’s define our F♭X language. The basic idea is very similar to the Return
semantics that we discussed above. We define a new kind of value,
Raise #ℓ v,
which bubbles up the exception #ℓ. This is the generalization of the value class Return v
from above. #ℓ is a metavariable representing an exception name. An exception contains
two pieces of data: a name, and an argument. The argument can be an arbitrary expres-
sion. Although we only allow single-argument exceptions, zero-valued or multi-valued
versions of exceptions can be easily encoded by, for example, supplying the exception with
a record argument with zero, one, or several fields. We also add to F♭X the expression
Note that Try binds free occurrences of x in e′ . Also notice that the F♭X Try syntax
differs slightly from the OCaml syntax in that OCaml allows an arbitrary pattern-match
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 78
expression in the With clause. We allow only a single clause that matches all values of
#ℓ.
F♭X is untyped, so exceptions do not need to be declared. We use the “#” symbol to
designate exceptions in the concrete syntax, for example, #MyExn. Below is an example
of a F♭X expression.
Exceptions are side-effects, and can cause “action at a distance.” Therefore, like any
other side-effects, they should be used sparingly.
Summarizing the above, here is the official grammar for F♭X, again expressed as an
extension of the F♭ grammar and using the same labels ℓ as we used for record fields and
variant names.
Note that we must handle the unusual case of when a Raise bubbles through a Raise,
something that will not happen very often in practice. The rule is very much like the “-
Raise Left” rule above.
e ⇒ Raise #ℓ v
(Raise Raise)
Raise #ℓ′ e ⇒ Raise #ℓ v
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 79
In addition, we need to add special versions of all of the other F♭ rules so that the
Raise bubbles up through the computation just as the Return did. As with Return, any
sub-component of evaluation could fail and in that case the whole thing must fail. But,
we need to make sure that all parts previous to the (first) failure were run beforehand.
Here for example are the two rules for -:
e ⇒ Raise #ℓ v
(- Raise Left)
e - e′ ⇒ Raise #ℓ v
e⇒v v not a Raise e′ ⇒ Raise #ℓ v
(- Raise Right)
e - e′ ⇒ Raise #ℓ v
Observe how we first must run e, and see it is not raising an exception, before running
e′ .
For completeness here are all of the other exception bubbling rules for the other
binary operators in F♭X. Yes they are all basically identical other than the fact that the
operator has changed.
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 80
e ⇒ Raise #ℓ v
(+ Raise Left)
e + e′ ⇒ Raise #ℓ v
e⇒v v not a Raise e′ ⇒ Raise #ℓ v
(+ Raise Right)
e + e′ ⇒ Raise #ℓ v
e ⇒ Raise #ℓ v
(= Raise Left)
e = e′ ⇒ Raise #ℓ v
e⇒v v not a Raise e′ ⇒ Raise #ℓ v
(= Raise Right)
e = e′ ⇒ Raise #ℓ v
e ⇒ Raise #ℓ v
(And Raise Left)
e And e′ ⇒ Raise #ℓ v
e⇒v v not a Raise e′ ⇒ Raise #ℓ v
(And Raise Right)
e And e′ ⇒ Raise #ℓ v
e ⇒ Raise #ℓ v
(Or Raise Left)
e Or e′ ⇒ Raise #ℓ v
e⇒v v not a Raise e′ ⇒ Raise #ℓ v
(Or Raise Right)
e Or e′ ⇒ Raise #ℓ v
e ⇒ Raise #ℓ v
(Application Raise Left)
e e′ ⇒ Raise #ℓ v
e⇒v v not a Raise e′ ⇒ Raise #ℓ v
(Application Raise Right)
e e′ ⇒ Raise #ℓ v
Note that there is a subtle point that these rules resolve: True + Raise #ee 4 will
return True + Raise #ee 4, we have not yet noticed that the + operator is ill-typed.
Here are the remaining bubbling rules.
CHAPTER 4. SIDE EFFECTS: STATE AND EXCEPTIONS 81
e ⇒ Raise #ℓ v
(Not Raise)
Not e ⇒ Raise #ℓ v
e1 ⇒ Raise #ℓ v
(If Raise Cond)
If e1 Then e2 Else e3 ⇒ Raise #ℓ v
e1 ⇒ True e2 ⇒ Raise #ℓ v
(If Raise True)
If e1 Then e2 Else e3 ⇒ Raise #ℓ v
e1 ⇒ False e3 ⇒ Raise #ℓ v
(If Raise False)
If e1 Then e2 Else e3 ⇒ Raise #ℓ v
e1 ⇒ Raise #ℓ v
(Let Raise Left)
Let x = e1 In e2 ⇒ Raise #ℓ v
e1 ⇒ v1 v1 not a Raise e2 [v1 /x] ⇒ Raise #ℓ v
(Let Raise Right)
Let x = e1 In e2 ⇒ Raise #ℓ v
Let x = 4 In
Try (If x = 0 Then 5 Else Raise #Return (4 + x)) - 8
With #Return n -> n
After the Let evaluation and the evaluation of the If statement, which raise no ex-
ceptions, we are left with
which is
Object-Oriented Language
Features
• Everyday objects are active, that is, they are not fully controlled by us. Objects
have internal and evolving state. For instance, a running car is an active object,
and has a complex internal state including the amount of gas remaining, the engine
coolant and transmission fluid levels, the amount of battery power, the oil level,
the temperature, the degree of wear and tear on the engine components, etc.
• Everyday objects are communicative. We can send messages to them, and we can
get feedback from objects as a result of sending them messages. For example,
starting a car and checking the gas gauge can both be viewed as sending messages
to the car.1
• Everyday objects are encapsulated. They have internal properties that we can not
see, although we can learn some of them by sending messages. To continue the car
example, checking the gas gauge tells us how much gas is remaining, even though
we can’t see into the gas tank directly, and we don’t know if it contains regular or
premium gas.
1
It may, at first, seem unnatural to view checking the gas gauge as sending a message. After all, it is
really the car sending a message to us. We view ourselves as the “sender,” however, because we initiated
the check, that is, in a sense, we asked the car how much gas was remaining.
82
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 83
• Everyday objects may be nested, that is, objects may be made up of several smaller
object components, and those components may themselves have smaller compo-
nents. Once again, a car is a perfect example of a nested object, being made of
of smaller objects including the engine and the transmission. The transmission is
also a nested object, including an input shaft, an output shaft, a torque converter,
and a set of gears.
• Everyday objects are, for the most part, uniquely named. Cars are uniquely named
by license plates or vehicle registration numbers.
• Everyday objects may be self-aware, in the sense that they may intentionally in-
teract with themselves, for instance a dog licking his paw.
The objects in object-oriented programming also have these properties. For this
reason, object-oriented programming has a natural and familiar feel to most people. Let
us now consider objects of the programming variety.
• Objects have an internal state in the form of instance variables or fields. Objects
are generally active, and their state is not fully controlled by their callers.
• Objects support messages, which are named pieces of code that are tied to a particu-
lar object. In this way objects are communicative, and groups of objects accomplish
tasks by sending messages to each other.
• Objects have unique names, or object references, to refer to them. This is analogous
to the naming of real-world objects, with the advantage that object references are
always unique whereas real-world object names may be ambiguous.
• Objects are self aware, that is, objects contain references to themselves. this in
Java and self in Smalltalk are self-references.
• Objects are polymorphic, meaning that a “fatter” object can always be passed to
a method that takes a “thinner” one, that is, one with fewer methods and public
fields.
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 84
Menu
Rendered Panel
Scroll Bar
There are several additional features objects commonly have. Classes are nearly
always present in languages with objects. Classes are not required: it is perfectly valid
to have objects without classes. The language Self [19, 20, 2] has no classes, instead it has
prototype objects which are copied that do the duty of classes. JavaScript copied this idea
from Self and so now many programmers are using prototype-based objects. Important
concepts of classes include creation, inheritance, method overriding, superclass access,
and dynamic dispatch. We will address these concepts later.
Information hiding for fields and methods is another feature that most object-oriented
languages have, generally in the form of public, private, and protected keywords.
Other aspects of objects include object types and modules. Types are discussed
briefly, but modules are beyond the scope of this book. We also ignore method over-
loading, as it is simply syntactic sugar and adds only to readability, not functionality.
Recall that overloading a method means there are two methods with the same name,
but different type signatures. Overriding means redefining the behavior of a superclass’s
method in a subclass. Overriding is discussed in Section 5.1.5.
Let point = {
x = 4;
y = 3;
magnitude = Fun -> . . . ;
iszero = Fun -> . . .
} In . . .
We can’t write the magnitude method yet, because we need to define it in terms of
x and y, but there is no way to refer to them in the function body. The solution we
will use is the same one that C++ uses behind the scenes: we pass the object itself as an
argument to the function. Let’s revise our first encoding to the following one (assuming
we’ve defined a sqrt function).
2
We use UML to diagram objects. UML is fully described in [11]. Although the diagram in Figure
5.2 is technically a class diagram, for our purposes we will view it simply as the induced object.
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 86
point
+x: int
+y: int
+magnitude(): int
+iszero(): boolean
Let point = {
x = 4;
y = 3;
magnitude = Fun this -> Fun ->
sqrt(sqr this.x + sqr this.y);
iszero = Fun this -> Fun ->
(([Link]) this {}) = 0
} In . . .
There are a few points of interest about the above example. First of all, the object
itself needs to be explicitly passed as an argument when we send a message. For example,
to send the magnitude message to point, we would write
[Link] point {}
For convenience, we can use the abbreviation obj <- method to represent the message
([Link] obj).
Even inside the object’s methods, we need to pass this along when we call another
method. iszero illustrates this point in the way it calls magnitude. This encoding of
self-reference is called the self-application encoding. There are a number of other
encodings, and we will treat some of them below.
There is a problem with this encoding, though; our object is still immutable. An
object with immutable fields can be a good thing in many cases. For example, windows
that can’t be resized and sets with fixed members can be implemented with immutable
fields. In general, though, we need our objects to support mutable fields. Luckily this
problem has an easy solution. Consider the following revision of our point encoding,
which uses Refs to represent fields.
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 87
Let point = {
x = Ref 4;
y = Ref 3;
magnitude = Fun this -> Fun ->
sqrt(sqr !(this.x) + sqr !(this.y));
iszero = Fun this -> Fun ->
([Link] this {}) = 0;
setx = Fun this -> Fun newx -> this.x := newx;
sety = Fun this -> Fun newy -> this.y := newy
} In . . .
To set x to 12, we can write either point <- setx 12, or we can directly change the
field with (point.x) := 12. To access the field, we now write !(point.x), or we could
define a getx method to abstract the dereferencing. This strategy gives us a faithful
encoding of simple objects. In the next few sections, we will discuss how to encode some
of the more advanced features of objects.
If we were to define person objects that supported the height message, we could pass
them as arguments to this function. However, we could also create specialized person
objects, such as mother, father, and child. As long as these objects still support
the height message, they are all valid candidates for the tallerThan function. Even
objects like dinosaurs and buildings can be arguments. The only requirement is that
any objects passed to tallerThan support the message height. This is known as object
polymorphism.
We already encountered a similar concept when we discussed record polymorphism.
Recall that a function
Let eqpoint = {
(* all the code from point above: x, y, magnitude . . . *)
equal = Fun this -> Fun apoint ->
!(this.x) = !(apoint.x) And !(this.y) = !(apoint.y)
} In eqpoint <- equal({
x = Ref 3;
y = Ref 7;
(* . . . *)
})
The object passed to equal needs only to define x and y. Object polymorphism in
our embedded language is thus more powerful than in C++ or Java: C++ and Java look
at the type of the arguments when deciding what is allowed to be passed to a method.
Subclasses can always be passed to methods that take the superclass, but nothing else is
allowed. In our encoding, which is closer to Smalltalk, any object is allowed to be passed
to a function or method as long as it supports the messages needed by the function.
One potential difficulty with object polymorphism is that of dispatch: since we
don’t know the form of the object until runtime, we do not know exactly where the
correct methods will be laid out in memory. Thus hashing may be required to look up
methods in a compiled object-oriented language. This is exactly the same problem that
arises when writing the record-handling code in our F♭SR compiler (see Chapter 8),
again illustrating the similarities between objects and records.
In this encoding, each method is “preapplied” to pointImpl, the full point object,
while pointInterface contains only public methods and instances. Methods are now
invoked simply as
[Link] 5
This solution has a flaw, though. Methods that return this re-expose the hidden fields
and methods of the full object. Consider the following example.
Let pointImpl = {
(* . . . *)
sneaky = Fun this -> Fun -> this
} In Let pointInterface = {
magnitude = [Link] pointImpl;
setx = [Link] pointImpl;
sety = [Link] pointImpl;
sneaky = [Link] pointImpl
} In [Link] {}
The sneaky method returns the full pointImpl object instead of the pointInterface
version. To remedy this problem, we need to change the encoding so that we don’t have
to pass this every time we invoke a method. Instead, we will give the object a pointer
to itself from the start.
[Link] {}
The method getThis still returns this, but this contains the public parts only. Note
that for this encoding to work, privateThis can be used only as a target for messages,
and can not be returned.
The disadvantage of this encoding is that this will need to be applied to itself every
time it’s used inside the object body. For example, instead of writing
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 90
([Link] {}).magnitude {}
These encodings are relatively simple. Classes and inheritance are more difficult
to encode, and are discussed below. Object typing is also particularly difficult, and is
covered in Chapter 6.
5.1.4 Classes
Classes are foremost templates for creating objects. A class is, in essence, an object
factory. Each object that is created from a class must have its own unique set of instance
variables (with the exception of static fields, which we will treat later).
It is relatively easy to produce a simple encoding of classes. Simply freeze the code
that creates the object, and thaw to create new object. Ignoring information hiding, the
encoding looks as follows.
We can define new pointClass to be pointClass {}. Some typical code which creates
and uses instances might look as follows.
point1 and point2 will have their own x and y values, since Ref creates new store cells
each time it is thawed. The same freeze and thaw trick can be applied to our encoding
of information hiding to get hiding in classes. The difficult part of encoding classes is
encoding inheritance, which we discuss in the next section.
A more useful notion of class is to not just freeze an object, but make a function
returning the object; the parameters to that function are analogous to the constructor
values of a class.
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 91
point
colorPoint +x: int
+color: RGBRecord +y: int
+brightness(): int +magnitude(): int
+setx(x:int): void
+sety(y:int): void
5.1.5 Inheritance
As a rule, about 80 percent of the utility of objects is realized in the concepts we have
talked about above, namely
• Objects that encapsulate data and code under a single name to achieve certain
functionality.
• Polymorphic objects.
The other 20 percent of the utility comes from from inheritance. Inheritance allows
related objects to be defined that share some common code. Inheritance can be encoded
in F♭SR by using the following scheme. In the subclass, we create an instance of the
superclass object, and keep the instance around as a “slave.” We use the slave to access
methods and fields of the superclass object. Thus, the subclass object only contains
new and overridden methods. Real object oriented languages tend not to implement
inheritance this way for reasons of efficiency (imagine a long inheritance chain in which
a method call has to be delegated all the way back to the top of the hierarchy before
it can be invoked). Still, the encoding is good enough to illustrate the main points of
inheritance. For example, consider the following encoding of ColorPoint, a subclass
of Point, illustrated in Figure 5.3. (In this encoding we will assume there are no class
constructor parameters, i.e. we will work from pointClass and not pointClass’ above.)
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 92
Let pointClass = . . . In
Let colorPointClass = Fun ->
Let super = pointClass {} In {
x = super.x; y = super.y;
color = Ref {red = 45; green = 20; blue = 20};
magnitude = Fun this -> Fun ->
mult([Link] this {})([Link] this {});
brightness = Fun this -> Fun ->
(* compute brightness. . . *)
setx = [Link]; sety = [Link]
} In . . .
There are several points of interest in this encoding. First of all, notice that to inherit
methods and fields from the superclass, we explicitly link them together (i.e. x, y, setx,
and sety). To override a method in the superclass, we simply redefine it in the subclass
instead of linking to the superclass; magnitude is an example of this. Also notice that we
can still invoke superclass methods from the subclass. For instance, magnitude invokes
[Link] in its body. Notice how [Link] is passed this instead of
super as its argument. This has to do with dynamic dispatch, which we will address
now.
the magnitude method here is not fixed at compile-time. Concretely, assuming pointClass
has this isNull and we execute
Let p = pointClass {} In
Let cp = colorPointClass {} In
p <- isNull {}; cp <- isNull {};
In p’s call to isNull, this is a point, and so isNull will internally invoke point’s
magnitude method. On the other hand for cp, this is a colorPoint, and so colorPoint’s
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 93
magnitude method will be invoked. In conclusion, the magnitude method call from
within isNull is dynamically dispatched.
The superclass dispatching above also now can be explained. If isNull in colorPointClass
had additionally been overridden to be
Fun this -> Fun -> ([Link] this {}) = 0 And (* etc *),
then tracing through cp’s call to isNull it would have correctly called the colorPointClass
magnitude, whereas if we had instead written
Fun this -> Fun -> ([Link] super {}) = 0 And (* etc *),
(note change in bold), the pointClass magnitude would have been invoked from within
isNull instead, and the brightness would mistakenly not have been taken into account
in the magnitude calculation for a colorPoint.
Another example For additional clarification on this issue let us do one more exam-
ple. Consider the following classes, rectClass and its subclass squareClass.
getLength = ([Link]);
Notice that in the squareClass, getLength has been overridden to behave the same
as getWidth. There are two ways to calculate the area in squareClass. areaStatic calls
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 94
([Link]) super {}. This means that when rectClass’s area method is invoked,
it is invoked with rectClass’s getLength and getWidth as well. The result is 1×10 = 10
since the rectangle was initialized to have width 1.
On the contrary, the dynamically dispatched area method, areaDynamic is written
([Link]) this {}. This time, rectClass’s area method is invoked, but this
is an instance of squareClass, rather than rectClass, and squareClass’s overridden
getLength and getWidth are used. The result is 1, the correct area for a square. The
behavior of dynamic dispatch is almost always the behavior we want. The key to dy-
namic dispatch is that inherited methods get a revised notion of this when they are
inherited. Our encoding promotes dynamic dispatch, because we explicitly pass this
into our methods.
Java (and almost every other object-oriented language) uses dynamic dispatch to
invoke methods by default. In C++, however, unless a method is declared as virtual, it
will not be dynamically dispatched.
Let pointClass = {
x = Ref newx;
y = Ref newy;
magnitude = Fun this -> Fun ->
sqrt ((!(this.x)) + (!(this.y)))
};
xdefault = 4;
ydefault = 3
} In
([Link]) point {}
Notice how the class method newWithXY is actually responsible for building the point
object. new simply invokes newWithXY with some default values that are stored as class
fields. This is a very clean way to encode multiple constructors.
Perhaps the most interesting thing the encoding is how classes with static fields start
to look like our original encoding of simple objects. Look closely—notice that class
methods take an argument class as their first parameter, for the exact same reason that
regular methods take this as a parameter. So in fact, pointClass is really just another
primitive object that happens to be able to create objects.
Viewing classes as objects is the dominant paradigm in Smalltalk. Java has some
support for looking at classes as objects through the reflection API as well. Even in
languages like C++ that don’t view classes as objects, design patterns such as the Factory
patterns[12] capture this notion of objects creating objects.
This encoding is truly in the spirit of object-oriented programming, and it is a clean,
and particularly satisfying way to think about classes and static members.
Let pointClass =
Class Extends EmptyClass
Inst
x = 0;
y = 0
Meth
magnitude = Fun -> sqrt(x + y);
setx = Fun newx -> x := newx;
sety = Fun newy -> y := newy
In Let colorPointClass =
Class Extends pointClass
Inst
x = 0;
y = 0;
(* A use of a primitive object: *)
color = Object
Inst
Meth red = 0; green = 0; blue = 0
Meth
magnitude =
Fun ->
mult(Super <- magnitude {})(This <- brightness {})
(* An unnormalized brightness metric *)
brightness = Fun ->
color <- red + color <- green + color <- blue;
setx = Super <- setx (* explicitly inherit *)
sety = ...; setcolor = ...
There is a lot going on with this syntax, so let’s take some time to point out some
of the major elements. First of all, notice that This and Super are special “reserved
variables.” In our F♭ encodings, we had to write “Fun this -> ” and pass this as an
explicit parameter. Now, self-awareness happens implicitly, and is This is a reference to
self.
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 97
Note the use of a primitive object to define the color field of the colorPointClass.
The red, green, and blue values are implemented as methods, since fields are always
“private.”
In our previous encodings we used one style when defining the base class, and another
different style when defining the subclass. In F♭OB we use the same syntax for both by
always specifying the superclass. To allow for base classes that do not inherit from any
other class, we allow a class to extend EmptyClass, which is simply a special class that
does not define anything.
F♭OB instance variables use the l/r-value form of state that was discussed in Section
4.1.3. There is no need to explicitly use the ! operator to get the value of an instance
variable. F♭OB instance variables are therefore mutable, not following the Caml conven-
tion that all variables are immutable. Note that method arguments are still immutable.
There are thus two varieties of variable: immutable method parameters, and mutable
instances. Since it is clear which variables are instances and which are not, there is no
great potential for confusion. It is the job of the parser to distinguish between instance
variables and regular variables. An advantage of this approach is that it keeps instance
variables from being directly manipulated by outsiders.
Method bodies are generally functions, but need not be; they can be any immutable,
publicly available value. For example, immutable instances can be considered methods
(see the color primitive object in the example above).
Note that we still have to explicitly inherit methods. This is not the cleanest syntax,
but it simplifies the interpreter, and facilitates the translation to F♭SR discussed in
Section 5.2.3.
Also, there is no constructor function. new is used to create new instances, and,
following Smalltalk, initialization is done explicitly by writing an initialize method.
For simplicity, F♭OB does not support static fields and methods, nor does it take
the “classes as objects” view discussed in the previous section.
Here is a rough sketch of the interpreter. This interpreter is not complete, but it gives a
general idea of what one should look like.
This code sketch for the interpreter does a nice job of illustrating the roles of This
and Super. We only substitute for this when we send a message. That’s because This is
a dynamic construct, and we don’t know what it will be until runtime (see the discussion
of dynamic dispatching in Section 5.1.6). On the other hand, Super is a static construct,
and is known at the time we write the code, which allows us to substitute for Super as
soon as the Class expression is evaluated.
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 99
The translation is fairly clean, except that messages to Super have to be handled a bit
differently than other messages in order to properly implement dynamic dispatch. Notice
again that instance variables are handled differently than function variables, because they
are mutable. Empty function application, i.e. f (), may be written as empty record
application: f {}. The “ ” variable in Fun -> e is any variable not occurring in e. The
character “ ” itself is a valid variable identifier in the FbDK implementation of F♭SR,
however.
As an example of how this translation works, let us perform it on the F♭OB version
of the point / colorPoint classes from Section 5.2.1. The result is the following:
Let pointClass =
Fun -> Let super = (Fun -> {}) {} In {
CHAPTER 5. OBJECT-ORIENTED LANGUAGE FEATURES 100
inst = {
x = Ref 3;
y = Ref 4
};
meth = {
magnitude = Fun this -> Fun ->
sqrt ((!([Link].x)) + (!([Link].y)));
setx = Fun this -> Fun newx ->
([Link].x) := newx;
sety = Fun this -> Fun newy ->
([Link].y) := newy
}
}
In Let colorPointClass =
Fun -> Let super = pointClass {} In {
inst = {
x = Ref 3;
y = Ref 4;
color = Ref ({inst = {}; meth = {
red = Fun this -> 45;
green = Fun this -> 20;
blue = Fun this -> 20
}})
};
meth = {
magnitude = Fun this -> Fun ->
mult (([Link]) this {})
(([Link]) this {});
brightness = Fun this -> Fun ->
(((!([Link])).[Link]) this) +
(((!([Link])).[Link]) this) +
(((!([Link])).[Link]) this);
setx = Fun this -> Fun newy ->
([Link]) this newy;
sety = Fun this -> Fun newy ->
([Link]) this newy;
setcolor = Fun this -> Fun c ->
([Link]) := c
}
} In
Interact with F♭SR. The translated code above should run fine in F♭SR, pro-
vided you define the mult and sqrt functions first. mult is easily defined as
sqrt is not so easily defined. Since we’re more concerned with the behavior of the
objects, rather than numerical accuracy, just write a dummy sqrt function that returns
its argument:
Now, try running it with the F♭SR file-based interpreter. Our dummy sqrt function
returns 7 for the point version of magnitude, and the colorPoint magnitude multiplies
that result by the sum of the brightness (85 in this case). The result is
$ FbSR [Link]
==> 595
After writing a Caml version of toFbSR for the abstract syntax, a F♭OB compiler is
trivially obtained by combining toFbSR with the functions defined in Chapter 8:
Finally, there are several other ways to handle these kinds of encodings. More infor-
mation about encoding objects can be found in [10].
Chapter 6
Type Systems
we will get some kind of interpreter-specific error at runtime. If F♭ had a type system,
such an expression would not have been allowed to evaluate.
In Lisp, if we define a function
and then call (f "abc"), the result is a runtime type error. Similarly, the Smalltalk
expression
results in a “message not supported” exception when run. Both of these runtime errors
could have been detected before runtime if the languages supported static type systems.
The C++ code
executes unknown and potentially harmful effects, but the equivalent Java code will throw
an ArrayIndexOutOfBoundsException at runtime, because array access is checked by a
dynamic type system.
These are just a few examples of the kinds of problems that type systems are designed
to address. In this chapter we discuss such type systems, as well as algorithms to infer
and to check types.
102
CHAPTER 6. TYPE SYSTEMS 103
• Java-style interfaces
There are also several newer dimensions of types that are currently active research areas.
• Effect types: the type “int -x,y-> int” indicates that variables x and y will
assigned to in this function. Java’s throws clauses for methods are a form of effect
types.
• Concrete class analysis: for variable x:Point, a concrete class analysis produces
a set such as {Point, ColorPoint, DataPoint}. This means at runtime x could
either be a Point, a ColorPoint, or a DataPoint (and, nothing else). This is useful
in optimization.
• Typed Assembly Language [3, 17]: put types on assembly-level code and have a
type system that guarantees no unsafe pointer operations.
• Logical assertions in types: int -> { x:int | odd(x) } for a function returning
odd numbers.
There is an important distinction that needs to be made between static and dynamic
type systems. Static type systems are what we usually mean when we talk about type
systems. A static type system is the standard notion of type found in C, C++, Java and
OCaml. Types are checked by the compiler, and type-unsafe programs fail to compile.
Dynamic type systems, on the other hand, check type information at runtime.
Lisp, Scheme, and Smalltalk are, in fact, dynamically typed. In fact, F♭ and F♭SR are
technically dynamically typed as well, since they will raise a typeMismatch when the
type of an expression is not what it expects. Any time you use a function, the runtime
environment makes sure that its a function. If you use an integer, it makes sure it’s an
integer, etc. These runtime type checks add a lot of overhead to the runtime environment,
and thus cause programs to run slowly.
There is some dynamic typechecking that occurs in statically typed languages too.
For instance, in Java, downcasts are verified at run-time and can raise exceptions. Out-
of-bounds array accesses are also checked at run-time in Java and OCaml, and are thus
dynamically typed. Array accesses are not typed in C or C++, since no check is performed
at all. Note that the type of an array (i.e. int, float) is statically checked, but the size
is dynamically checked.
Finally, languages can be untyped. The FbSR compiler produces untyped code,
since runtime errors cause core dumps. It is important to understand the distinction
between an untyped language and a dynamically typed one. In an untyped language
there is no check at all and anomalous behavior can result at runtime. In Chapter 8,
we compile F♭SR to untyped C code, using casts to “disable” type system. Machine
language is another example of an untyped language.
To really see the difference between untyped and dynamically typed languages, con-
sider the following two program fragments. The first is C++ code with the type system
“disabled” via casts.
CHAPTER 6. TYPE SYSTEMS 105
#include <iostream>
class Calculation {
public: virtual int f(int x) { return x; }
};
class Person {
public: virtual char *getName() { return "Mike"; }
};
return 0;
}
The code compiles with no errors, but when we run it the output is “ã¿.” But if
we compile it with optimization, the result is “Ãà.” Run it on a different computer and
it may result in a segmentation fault. Use a different compiler, and the results may be
completely exotic and unpredictable. The point is that because we’re working with an
untyped language, there are no dynamic checks in place, and meaningless code like this
results in undefined behavior.
Contrast this behavior with that of the equivalent piece of Java code. Recall that
Java’s dynamic type system checks the type of the object when a cast is made. We will
use approximately the same code:
class Calculation {
public int f(int x) { return x; }
}
class Person {
public String getName() { return "Mike"; }
}
class Main {
public static void main(String[] args) {
Object o = new Calculation();
[Link](((Person) o).getName());
}
}
Fun x -> x + 1,
For TF♭ and TF♭SRX, we require argument types to all be declared, but we infer
return types for simplicity. So functions are of the form
We should also illustrate the difference between type checking and type inference.
In any typed language, the compiler should typecheck the program before generating
CHAPTER 6. TYPE SYSTEMS 107
code. Type inference algorithms infer types and check that the program body has
no type errors. OCaml is an example of this.
A type checker generally just checks the body is well-typed given the types listed
on declarations. This is how C, C++, and Java work, although technically they are also
inferring some types, such as the type of 3+4.
In any case it must be possible to run the type inference or type checking algorithm
quickly. OCaml in theory can take exponential time to infer types, but in practice it is
linear.
Type Systems
Type systems are rule-based formal systems that are similar to operational semantics.
Type systems rigorously and formally specify what program have what types, and have
a strong and deep parallel with formal logic (recall our discussion of Russell’s Paradox
in Section 2.3.5). Type systems are generally a set of rules about type assertions.
Definition 6.1. (Type Environment) A type environment, Γ, is a set {x1 : τ1 , . . . , xn :
τn } of bindings of free variables types. If a variable x is listed twice in Γ, the rightmost
(innermost) binding is the proper type. We write Γ(x) = τ to indicate that τ is the
innermost type for x in Γ.
Definition 6.2. (Type Assertion) A type assertion, Γ ⊢ e : τ , indicates that in type
environment Γ, e is of type τ .
The TF♭ types in the concrete syntax are
Fun x:τ → e
where the abstract syntax representation in OCaml is
CHAPTER 6. TYPE SYSTEMS 108
For completeness here is the full grammar of the TF♭ syntax and types.
x ::= . . . variables
v ::= x | True | False | 0 | 1 | -1 | 2 | -2 | . . .
| Fun x:τ → e values
e ::= v | (e) | e And e | e Or e | Not e | e + e | e - e | e = e
| e e | If e Then e Else e | Let x:τ = e In e expressions
τ ::= Int | Bool | τ -> τ
Notice that the Let declarations also include the type on the variable x. This is not
strictly needed as it can be inferred, but we put it in following the standard convention
in typed languages such as C and Java.
(Hypothesis)
Γ ⊢ x : τ for Γ(x) = τ
(Int)
Γ ⊢ n : Int for n an integer
(Bool)
Γ ⊢ b : Bool for b True or False
The Hypothesis rule simply says that if a variable x contained in a type environment
Γ has type τ then the assertion Γ ⊢ x : τ is true. The Int and Bool rules simply give
types to literal expressions such as 7 and False. These rules make up the base cases of
our type system.
Next, we have rules for simple arithmetic expressions.
Γ ⊢ e : Int, Γ ⊢ e′ : Int
(+)
Γ ⊢ e + e′ : Int
Γ ⊢ e : Int, Γ ⊢ e′ : Int
(-)
Γ ⊢ e - e′ : Int
Γ ⊢ e : Int, Γ ⊢ e′ : Int
(=)
Γ ⊢ e = e′ : Bool
These rules are fairly straightforward. For addition and subtraction, the operands
must typecheck to Ints, and the result of the expression is an Int. Equality is similar,
CHAPTER 6. TYPE SYSTEMS 109
but typechecks to a Bool. Note that equality will only typecheck with integer operands,
not boolean ones. The And, Or, and Not rules are very similar.
Γ ⊢ e : Bool, Γ ⊢ e′ : Bool
(And)
Γ ⊢ e And e′ : Bool
Γ ⊢ e : Bool, Γ ⊢ e′ : Bool
(Or)
Γ ⊢ e Or e′ : Bool
Γ ⊢ e : Bool
(Not)
Γ ⊢ Not e : Bool
The If rule is a bit more complicated. Clearly, the conditional part of the expression
must typecheck to a Bool. But what about the Then and Else clauses? Consider the
following expression.
Should this expression typecheck? If e evaluates to True, then the result is 3, an Int.
If e is False, the result is False, a Bool. Clearly, then this expression should not
typecheck, because it does not always evaluate to the same type. This tells us that for
an If statement to typecheck, both clauses must typecheck to the same type, and the
type of the entire expression is then the same as the two clauses. The rule is as follows.
Γ ⊢ e : Bool Γ ⊢ e′ : τ Γ ⊢ e′′ : τ
(If)
Γ ⊢ If e Then e′ Else e′′ : τ
We have now covered all the important rules except for Let, functions, and appli-
cation. The Let and Function rules are a bit different from other rules, because they
introduce new variables. In Let x:τ = e In e′ , x can be used in e′ and it should be
known to be of type τ there, thanks to the Hyp rule above. To support this, we append
x : τ to Γ when typing e′ :
Γ ⊢ e : τ Γ, x : τ ⊢ e′ : τ ′
(Let)
Γ ⊢ Let x:τ = e In e′ : τ ′
Similarly, the type of x the function body also needs to be in Γ.
Γ, x : τ ⊢ e : τ ′
(Function)
Γ ⊢ (Fun x:τ -> e) : τ -> τ ′
Notice the use of the type constructor -> to represent the type of the entire function
expression. This type constructor should be familiar, as the OCaml type system uses the
same notation. In addition, the Function rule includes the addition of an assumption
to Γ. We assume the function argument, x, is of type τ , and add this assumption to the
environment Γ to derive the type of e. The Application rule follows from the Function
rule:
CHAPTER 6. TYPE SYSTEMS 110
Γ ⊢ e : τ -> τ ′ Γ ⊢ e′ : τ
(Application)
Γ ⊢ e e′ : τ ′
Just as in operational semantics, a derivation of Γ ⊢ e : τ is a tree of rule applications
where the leaves are axioms (Hypothesis, Int or Bool rules) and the root is Γ ⊢ e : τ .
Let’s try an example derivation.
⊢ f 5 True : Int
Because by the application rule,
⊢ f : Int -> Bool -> Int
(which we derived above)
⊢ 5 : Int by the Int rule
And thus
⊢ f 5 : Bool -> Int by the Application rule.
Given this and
⊢ True : Bool by the Bool rule
we can get
⊢ f 5 True : Int by the Application rule.
Now that we have a type system for the TF♭ language, we can detect whether or not
our programs are well-typed. But what does this mean? The answer comes in the form
of the following type soundness theorem.
We will not precisely define the concept of a stuck state. It is basically a point at
which evaluation can not continue, such as 0 (Fun x -> x) or (Fun x -> x) + 4. In
terms of a F♭ interpreter, stuck stated are the cases that raise exceptions. This theorem
asserts that a type system prevents runtime errors from occurring. Similar theorems are
the goal of most type systems.
Lemma 6.1. typecheck faithfully implements the TF♭ type system. That is,
This Lemma implies the typecheck function is a sound implementation of the type
system for TF♭.
and fbtype =
Int | Bool | Arrow of fbtype * fbtype
| Rec of label * fbtype list | Rf of fbtype
CHAPTER 6. TYPE SYSTEMS 113
We will let the now-experienced reader infer the full BNF grammar for TF♭SRX
from the above OCaml abstract syntax definition.
Next, we will define the type rules for TF♭SRX. All of the TF♭ type rules apply, and
so we can move directly to the more interesting rules. Let’s begin by tackling recursion.
What we’re really typing is the In clause, but we need to ensure that the rest of the
expression is also well-typed.
Γ, f : τ -> τ ′ , x : τ ⊢ e : τ ′ , Γ, f : τ -> τ ′ ⊢ e′ : τ ′′
(Let Rec)
Γ ⊢ (Let Rec f x:τ = e:τ ′ In e′ ) : τ ′′
Next, we move on to records and projection. The type of a record is simply a map of
the field names to the types of the values associated with each field. Projection is typed
as the type of the value of the field projected. The rules are
Γ ⊢ e1 : τ1 , . . . , Γ ⊢ en : τn
(Record)
Γ ⊢ { ℓ1 = e1 ; . . . ; ℓn = en } : { ℓ1 : τ1 ; . . . ; ℓn : τn }
Γ ⊢ e : { ℓ1 : τ1 ; . . . ; ℓn : τn }
(Projection)
Γ ⊢ e.ℓi : τi for 1 ≤ i ≤ n
We’ll also need to be able to type side effects. We can type Ref expressions with the
special type τ Ref. Set and Get expressions easily follow.
Γ⊢e:τ
(Ref)
Γ ⊢ Ref e : τ Ref
Γ ⊢ e : τ Ref, Γ ⊢ e′ : τ
(Set)
Γ ⊢ e := e′ : τ
Γ ⊢ e : τ Ref
(Get)
Γ ⊢ !e : τ
Finally, the other kind of side effects we need to type are exceptions. To type an
exception itself, we will simply use the type Exn. This allows all exceptions to be typed
in the same way. For example, the code
will typecheck, and has type Exn. We do this to allow maximum flexibility.
Because they alter the flow of evaluation, Raise expressions should always typecheck,
provided the argument typechecks to an Exn type. It is difficult to know what type to
give a raise expression, though. Consider the following example.
This expression should typecheck to type Int. From our If rule, however, we know that
the Then and Else clause must have the same type. We infer, therefore, that the Raise
expression must have type Int for the If to typecheck. In Section 6.6.2 we see how to
handle this inference automatically. For now, we will simply type Raise expressions with
the arbitrary type τ . Note that this is a perfectly valid thing for a type rule to do, but
it is difficult to implement in an actual typechecker.
Next, notice that the With clause of the Try expression is very much like a function.
Just as we did with functions, we will need to decorate the identifier with type information
as well. However, as we see below, this decoration can be combined with the final kind
of type decoration, which we will discuss now.
Consider the expression
Γ ⊢ e : τ′
(Raise)
Γ ⊢ (Raise #ℓ@τ ′ e) : τ for arbitrary τ
Γ ⊢ e : τ, Γ, x : τ ′ ⊢ e′ : τ
(Try)
Γ ⊢ (Try e With #ℓ@τ ′ x -> e′ ) : τ
CHAPTER 6. TYPE SYSTEMS 115
Therefore, by the Try rule, we deduce the type Int for the original expression.
Exercise 6.2. Why are there no type rules for cells?
Exercise 6.3. How else could we support recursive functions in TF♭SRX without using
Let Rec, but still requiring that recursive functions properly typecheck? Prove that your
solution typechecks.
Exercise 6.4. Attempt to type some of the untyped programs we have studied up to
now, for example, the Y -combinator, Let, sequencing abbreviations, a recursive factorial
function, and the encoding of lists. Are there any that can not typecheck at all?
Exercise 6.5. Give an example of a non-recursive F♭SR expression that evaluates prop-
erly to a value, but does not typecheck when written in TF♭SRX.
6.5 Subtyping
The type systems that we covered above are reasonably adequate, but there are still
many types of programs that have no runtime errors that will nonetheless not typecheck.
The first extension to our standard type systems that we would like to consider is what
is known as subtyping. The main strength of subtyping is that it allows record and
object polymorphism to typecheck.
Subtypes should already be a familiar concept from Java and C++. Subclasses are
subtypes, and extending or implementing an interface gives a subtype.
6.5.1 Motivation
Let us motivate subtypes with an example. Consider a function
This function takes as an argument a record with field l of type Int. In the untyped
F♭R language the record passed into the function could also include other fields besides
l, and the call
would generate no run-time errors. However, this would not type-check by our TF♭SRX
rules: the function argument type is different from the type of the value passed in.
The solution is to re-consider record types such as {m:Int; n:Int} to mean a record
with at least the m and n fields of type Int, but possibly other fields as well, of unknown
type. Think about the previous record operations and their types: under this interpre-
tation of record typing, the Record and Projection rules both still make sense. The old
rules are still sound, but we need a new rule to reflect this new understanding of record
types:
Γ ⊢ e : { ℓ1 : τ1 ; . . . ; ℓn : τn }
(Sub-Record0 )
Γ ⊢ e : { ℓ1 : τ1 ; . . . ; ℓm : τm } for m < n
This rule is valid, but it’s not as good as we could do. To see why, consider another
example,
Γ ⊢ e : { ℓ1 : τ1 ; . . . ; ℓn : τn } -> τ
(Sub-Function0 )
Γ ⊢ e : { ℓ1 : τ1 ; . . . ; ℓn : τn ; . . . ; ℓm : τm } -> τ
Using this rule, F G will indeed typecheck. The problem is that we still need other rules.
Consider records inside of records:
6.5.2 The STF♭R Type System: TF♭ with Records and Subtyping
By now it should be clear that the strategy we were trying to use above can never
work. We would need a different type rule for every possible combination of records and
functions!
The solution is to have a separate set of subtyping rules just to determine when one
type can be used in the place of another. τ <: τ ′ is read “τ is a subtype of τ ′ ,” and means
that an object of type τ may also be considered an object of type τ ′ . This principle is
called subsumption and will be incoroporated into the STF♭R type rules below.
Before proceeding with the rules let us clarify the syntax we are working over: we
are basically taking the TF♭ grammar and type rules, and adding records and subtyping
principles.
To summarize, that means our STF♭R grammar is
x ::= . . . variables
ℓ ::= . . . labels
v ::= x | True | False | 0 | 1 | -1 | 2 | -2 | . . .
| Fun x:τ → e | { ℓ = v; . . . ; ℓ = v } values
e ::= v | (e) | e And e | e Or e | Not e | e + e | e - e | e = e | e e
| If e Then e Else e | Let x:τ = e In e | { ℓ = e; . . . ; ℓ = e } expressions
τ ::= Int | Bool | τ -> τ | { ℓ : τ ; . . . ; ℓ : τ } types
The type rules of STF♭R start with all of the rules of TF♭ found in Section 6.2.2.
Then, we also include the record and projection typing rules as in TF♭SRX:
Γ ⊢ e1 : τ1 , . . . , Γ ⊢ en : τn
(Record)
Γ ⊢ { ℓ1 = e1 ; . . . ; ℓn = en } : { ℓ1 : τ1 ; . . . ; ℓn : τn }
Γ ⊢ e : { ℓ1 : τ1 ; . . . ; ℓn : τn }
(Projection)
Γ ⊢ e.ℓi : τi for 1 ≤ i ≤ n
Γ ⊢ e : τ, ⊢ τ <: τ ′
(Sub)
Γ ⊢ e : τ′
The subtyping relation τ <: τ then needs to have its own set of rules to deduce when
one type can subsume another. First we need to make our subtyping operator reflexive
and transitive. This can be accomplished with the following two rules.
(Sub-Refl)
⊢ τ <: τ
⊢ τ <: τ ′ , ⊢ τ ′ <: τ ′′
(Sub-Trans)
⊢ τ <: τ ′′
CHAPTER 6. TYPE SYSTEMS 118
Our rule for subtyping records needs to do two things. It needs to ensure that if a
record B is the same as record A with some additional fields, then B is a subtype of A.
It also needs to handle the case of records within records. If B’s fields are all subtypes
of A’s fields, then B should also be a subtype of A. We can reflect this concisely in a
single rule as follows.
(Sub-Record)
⊢ τ1 <: τ1′ , . . . , τn <: τn′
⊢ { ℓ1 : τ1 ; . . . ; ℓn : τn ; . . . ; ℓm : τm } <: { ℓ1 : τ1′ ; . . . ; ℓn : τn′ }
Note that we assume record fields can be implicitly re-ordered, and in this rule the
common fields are placed in the front of the record type on the left of the conclusion.
The function rule must also do two things. First if functions A and B are equivalent
except that B returns a subtype of what A returns, then B is a subtype of A. However,
if A and B are the same except that B’s argument is a subtype of A’s argument, then A
is a subtype of B. Simply put, for a function to be a subtype of another function, it has
to take less and give more. The rule expresses this intuition:
From our discussions and examples in the previous section, it should be clear that
this more general set of rules will work.
Γ ⊢ e : τ -> τ ′ , Γ ⊢ e′ : τ
(Application)
Γ ⊢ e e′ : τ ′
In STF♭R at this point, we need to see if subtyping is possible. typecheck(e′ ) returns
τ ′′ and then τ ′′ <: τ is checked via a function areSubtypes(τ ′′ , τ ). This produces a valid
proof by the Sub rule. Other rules where the TF♭ rules require a type match similarly
are generalized to allow the Sub rule to be used.
What principality means is no other typing will let more uses of the program type-
check, so the principal typing will always be best. The desired property of our type
inference algorithm is that it will always infer principal types. An important corollary is
that with a principal-type algorithm, we know inference will never “get in the way” of
the programmer by, for example, inferring Bool -> Bool for the identity function when
the programmer wants to use it on Int -> Int.
OCaml infers the type ’a -> ’a for the identity function, which can be shown to be
a principal type for it. In fact, OCaml type inference always infers principal types.
CHAPTER 6. TYPE SYSTEMS 120
It is a fact that if there are no inconsistencies in the equations, they can always be
simplified to give an equation-free type.
is an equational type. If you think about it, this is really the same as the type
This is known as equation simplification and is a step we will perform in our type inference
algorithm. It is also possible to write meaningless types such as
which cannot be a type since it implies that functions and booleans are the same type.
Such equation sets are deemed inconsistent, and will be equated with failure of the type
inference process. There are also possibilities for circular (self-referential) types that
don’t quite look inconsistent:
(Hypothesis)
Γ ⊢ x : τ \∅ for Γ(x) = τ
(Int)
Γ ⊢ n : Int\∅ for n an integer
(Bool)
Γ ⊢ b : Bool\∅ for b a boolean
The rules for +, -, and = also look similar to their TF♭ counterparts, but now we must
take the union of the equations of each of the operands to be the set of equations for the
type of the whole expression, and add an equation to reflect the type of the operands.
Γ ⊢ e : τ \E, Γ ⊢ e′ : τ ′ \E ′
(+)
Γ ⊢ e + e′ : Int\E ∪ E ′ ∪ {τ = Int, τ ′ = Int}
Γ ⊢ e : τ \E, Γ ⊢ e′ : τ ′ \E ′
(-)
Γ ⊢ e - e′ : Int\E ∪ E ′ ∪ {τ = Int, τ ′ = Int}
Γ ⊢ e : τ \E, Γ ⊢ e′ : τ ′ \E ′
(=)
Γ ⊢ e = e′ : Bool\E ∪ E ′ ∪ {τ = Int, τ ′ = Int}
The And, Or, and Not rules are defined in a similar way.
Γ ⊢ e : τ \E, Γ ⊢ e′ : τ ′ \E ′
(And)
Γ ⊢ e And e′ : Bool\E ∪ E ′ ∪ {τ = Bool, τ ′ = Bool}
Γ ⊢ e : τ \E, Γ ⊢ e′ : τ ′ \E ′
(Or)
Γ ⊢ e Or e′ : Bool\E ∪ E ′ ∪ {τ = Bool, τ ′ = Bool}
Γ ⊢ e : τ \E
(Not)
Γ ⊢ Not e : Bool\E ∪ {τ = Bool}
CHAPTER 6. TYPE SYSTEMS 122
The rule for If is also similar to the TF♭ If rule. Notice, though, that we do not
immediately infer a certain type like we did in the previous rules. Instead, we infer a
type α, and equate α to the types of the Then and Else clauses.
(If)
Γ ⊢ e : τ \E, Γ ⊢ e′ : τ ′ \E ′ , Γ ⊢ e′′ : τ ′′ \E ′′
Γ ⊢ (If e Then e′ Else e′′ ) : α\E ∪ E ′ ∪ E ′′ ∪ {τ = Bool, τ ′ = α, τ ′′ = α}
Finally, we are ready for the let, function and application rules. Functions no longer
have explicit type information, but we may simply choose a fresh type variable ’a as the
function argument, and include it in the equations later. The application rule also picks
a fresh type variable ’a type, and adds an equation with ’a as the right hand side of a
function type.
Γ ⊢ e : τ \E Γ, x : α ⊢ e′ : τ ′ \E ′
(Let)
Γ ⊢ (Let x = e In e′ ) : τ ′ \E ∪ E ′ ∪ {α = τ }
Γ, x : α ⊢ e : τ \E
(Function)
Γ ⊢ (Fun x -> e) : α -> τ \E
Γ ⊢ e : τ \E Γ ⊢ e′ : τ ′ \E ′
(Application)
Γ ⊢ e e′ : α\E ∪ E ′ ∪ {τ = τ ′ -> α}
These rules almost directly define the equational type inference procedure: the proof
can pretty much be built from the bottom (leaves) on up. Each equation added denotes
two types that should be equal.
• For each equation of the form τ0 -> τ0′ = τ1 -> τ1′ in E, add τ0 = τ1 and τ0′ = τ1′
to E.
Note that we will implicitly use the symmetric property on these equations, and so there
is no need to add τ1 = τ0 for every equation τ0 = τ1 .
The closure serves to uncover inconsistencies. For instance,
2. No self-referential equations exist (we will deal with this issue shortly).
If the equations are consistent, the next step is to solve the equational constraints.
We do this by substituting type variables with actual types. The algorithm is as follows.
Given τ \E,
Notice that step (1) considers the symmetric equivalent of each equation, which is why
we didn’t include them in the closure. The algorithm has a flaw though: the replacements
may continue forever. This happens when E contains a circular type. Recall the example
of a self-referential type
The solution is to check for such cycles before trying to solve the equations. The
best way to do this it to phrase the problem in graph-theoretical context. Specifically,
CHAPTER 6. TYPE SYSTEMS 124
we define a directed graph G in which the nodes are the the type variables in E. There
is a directed edge from ’a to ’b if ’a = τ is an equation in E and ’b occurs in τ .
We raise a typeError if there is a cycle in G for which there is at least one edge
representing a constraint that isn’t just between type variables (’a = ’b).
In summary, our entire EF♭ type inference algorithm is as follows. For expression e,
1. Produce a proof of ⊢ e : τ \E by applying the EF♭ type rules. Such a proof always
exists.
4. Check for cycles in E using the algorithm described above. If there is a cycle, raise
a typeError.
5. Solve E by the above equation solution algorithm. This algorithm will always
terminate if there are no cycles in E.
Theorem 6.2. The typings produced by the above algorithm are always principal.
{’a = Bool, Int = ’b, ’a -> ’b = Bool -> ’c, ’b = ’c, Int = ’c}
The set is not immediately inconsistent, and does not contain any cycles. Therefore, we
solve the equations. In this case, τ contains only ’c, and we can replace ’c with Int.
We output Int as the type of the expression, which is clearly correct.
In OCaml, such programs typecheck fine. Different uses of Fun y -> y can have
different types. Consider what EF♭ would do when typing this expression, though.
But when we compute the closure of this equational type, we get the equation Int =
Bool! What went wrong? The problem in this case is that each use of x in the body
used the same type variable ’a. In fact, when we type Fun y -> y, we know that ’a
can be anything, so for different uses, ’a can be different things. We need to build this
intuition into our type system to correctly handle cases like this. We define such a type
system in PEF♭, which is EF♭ with Let-polymorphism.
PEF♭ has a special Let typing rule replacing the EF♭ rule, in which we allow a new
kind of type in Γ: ∀α1 . . . αn . τ . This is called a type schema, and may only appear in
Γ. An example of a type schema is ∀α. α -> α. Note that the type variables α1 . . . αn
are considered to be bound by this type expression.
The new rule for Let in PEF♭ is
Γ ⊢ e : τ \E, Γ, x : ∀α1 . . . αn . τ ′ ⊢ e′ : τ ′′ \E ′
(Let)
Γ ⊢ (Let x = e In e′ ) : τ ′′ \E ∪ E ′
CHAPTER 6. TYPE SYSTEMS 126
where τ ′ is a solution of ⊢ e : τ \E using the above algorithm, and τ ′ has free type
variables α1 . . . αn that do not occur in Γ.
Notice that since we are invoking the simplification algorithm in this rule, it means
the full algorithm is not the clean 3-pass infer-closure-simplify form give above: the rules
need to call close-simplify on some sub-derivations.
We also need to add an axiom to ensure that a fresh type variable is given to each
Let usage. The rule is
(Let Inst.)
Γ, x : ∀α1 . . . αn . τ ′ ⊢ x : R(τ ′ )\∅
where R(τ ′ ) is a renaming of the variables α1 . . . αn to fresh names. Since these names
are fresh each time x is used, the different uses won’t conflict like above.
It will help to see an example of this type system in action. Let’s type the example
program from above:
CF♭R has the following set of type rules. These are direct generalizations of the EF♭
rules, replacing = by <:. The <: is always in the direction of information flow. We let
C represent a set of subtyping constraints.
(Hypothesis)
Γ ⊢ x : τ \∅ for Γ(x) = τ
(Int)
Γ ⊢ n : Int\∅ for n an integer
(Bool)
Γ ⊢ b : Bool\∅ for b a boolean
Γ ⊢ e : τ \C, Γ ⊢ e′ : τ ′ \C ′
(+)
Γ ⊢ e + e′ : Int\C ∪ C ′ ∪ {τ <: Int, τ ′ <: Int}
Γ ⊢ e : τ \C, Γ ⊢ e′ : τ ′ \C ′
(-)
Γ ⊢ e - e′ : Int\C ∪ C ′ ∪ {τ <: Int, τ ′ <: Int}
Γ ⊢ e : τ \C, Γ ⊢ e′ : τ ′ \C ′
(=)
Γ ⊢ e = e′ : Bool\C ∪ C ′ ∪ {τ <: Int, τ ′ <: Int}
(If)
Γ ⊢ e : τ \C, Γ ⊢ e′ : τ ′ \C ′ , Γ ⊢ e′′ : τ ′′ \C ′′ ,
Γ ⊢ (If e Then e′ Else e′′ ) : α\C ∪ C ′ ∪ C ′′ ∪ {τ <: Bool, τ ′ <: α, τ ′′ <: α}
Γ, x : α ⊢ e : τ \C
(Function)
Γ ⊢ (Fun x -> e) : α -> τ \C
Γ ⊢ e : τ \C, Γ ⊢ e′ : τ ′ \C ′
(Application)
Γ ⊢ e e′ : α\C ∪ C ′ ∪ {τ <: τ ′ -> α}
CHAPTER 6. TYPE SYSTEMS 128
The two rules we have not seen in EF♭ are the Record and Projection rules. There is
nothing particularly special about these rules, however.
Γ ⊢ e1 : τ1 \C1 , . . . , Γ ⊢ en : τn \Cn
(Record)
Γ ⊢ { ℓ1 = e1 ; . . . ; ℓn = en } : { ℓ1 : τ1 ; . . . ; ℓn : τn }\C1 ∪ . . . ∪ Cn
Γ ⊢ e : τ \C
(Projection)
Γ ⊢ e.ℓ : α\{τ <: { ℓ : α}} ∪ C
As with EF♭, these rules almost directly define the type inference procedure and the
proof can pretty much be built from the bottom up.
The complete type inference algorithm is as follows. Given an expression e,
1. Produce a proof of ⊢ e : τ \C using the above type rules. Such a proof always
exists.
The algorithms for computing the closure of C and doing cycle detection are fairly
obvious generalizations of the EF♭ algorithms. Closure is computed as follows.
2. For each constraint τ0 -> τ0′ <: τ1 -> τ1′ in C, add τ1 <: τ0 and τ0′ <: τ1′ to C.
A constraint set is immediately inconsistent if τ <: τ ′ and τ and τ ′ are different kinds
of type (function and record, Int and function, etc), or two records are ordered by <:
and the right record has a field the left record does not.
To perform cycle detection in C, we use the following algorithm. Define a directed
graph G where nodes are type variables in C. There is an edge from a ’a node to a ’b
node if there is an equation ’a <: τ ′ in C, and ’b occurs in τ ′ . Additionally, there is an
edge from ’b to ’a if τ ′ <: ’a occurs in C and ’b occurs in τ ′ . C has a cycle if and only
if G has a cycle.
It seems that there is a major omission in our constrained type inference algorithm: we
never solve the constraints! The algorithm is correct, however. The reason we don’t want
to solve the constraints is that any substitution proceeds with possible loss of generality.
Consider, for example, a constraint ’a <: τ , and the possibility of substituting ’a with τ .
This precludes the possibility that the ’a position be a subtype of τ , as the substitution in
CHAPTER 6. TYPE SYSTEMS 129
effect asserts the equality of ’a and τ . In simpler terms, we need to keep the constraints
around as part of the type. This is the main weakness of constrained type systems; the
types include the constraints, and are therefore difficult to read and understand.
We have the same shortcomings as in the equational case at this point: there is as
of yet no polymorphism. The solution used in the equational case won’t work here, as it
required the constraints to be solved.
The solution is to create constrained polymorphic types
∀α1 , . . . , αn . τ \C
in the assumptions Γ, in place of the polymorphic types (type schema) we had in the
equational version. The details of this process are quite involved, and we will not go into
them. Constrained polymorphic types make very good object types, since polymorphism
is needed to type inheritance.
Chapter 7
Concurrency
A concurrent computation is working on more than one thing at once. We assume some
familiarity with concurrency but provide a brief overview to get us going. The Wikipedia
article on parallel computing provides a more detailed overview.
7.1 Overview
Concurrent execution can be loosely grouped into three implementation categories, in
order of loosest to tightest coupling. Distributed computation is computation on mul-
tiple computers which share no memory and are sending messages between each other
to communicate data. There is still a wide range of how tightly these computers can
be decoupled. Grid computing is distributed computing where the Internet is the com-
munication medium. Cluster computing is over a fast LAN network. Massive Parallel
Processing (MPP) involves specialized communication hardware for very high commu-
nication bandwidth. Distributed shared memory is the case where different processes
are running on different processors but sharing some special memory via a memory
bus. Lastly, multithreaded computation is the case where multiple threads of execution
share a single memory which is local. Multithreaded computations may run on a single
(core) CPU, meaning the concurrent execution is an illusion achieved by interleaving
the execution steps of two threads, or if the computer has multiple cores there can be
true concurrent execution of a multithreaded program. The Threads Wikipedia article
clarifies how threads and processes differ.
All of the above models still support independent foci of control. That is the primary
focus of our study in this chapter – it captures a wide range of models and they are
the most elegant forms of concurrent architecture. There are several other models that
we are not addressing. Vector processors are computers that can do an operation on a
whole array in one step. These architectures used to be called SIMD (Single Instruction
Multiple Data). Stream processors are the modern version of vector processors which
can work on more than just arrays in parallel, for example sparse arrays. The GPGPU
(General Purpose Graphics Processing Units) is a recent version of a stream processor
which arose as a generalization of specialized graphics processors. Lastly, FPGA’s are
Field Programmable Circuits: you can create your own (parallel) logic circuitry on the
fly.
130
CHAPTER 7. CONCURRENCY 131
not atomicity.
The Java concurrency model has some other nice features which we briefly review
here. Much of this is found in the [Link] package.
• Atomic integers, floats, etc - there will never be any race conditions on setting or
getting the value from AtomicInteger etc.
• Locks - [Link]
where a are taken from an unbounded set of actor names. They are like the cells c of
F♭S in that they cannnot appear in source programs but can show up at runtime, and
there are infinitely many unique ones; they are just names (nonces).
AF♭V syntax e e′ indicates a message send; it expects e to evaluate to an actor
name, and sends that actor the message which is the value of e′ . Create(e, e′ ) creates
an actor with behavior e, and with initial local data e′ . e should evaluate to a function
and that function is the (whole) code for the actor. The Create returns the (new) name
of this new actor as its result.
Some features of AF♭V include the following. The code of an actor is nothing but one
function – there is no state within an actor in the form of fields. At the end of processing
each message, the actor goes idle; the behavior it is going to have upon receiving the next
message is the value at the end of the previous message send. The latter point is the key
to how actors can mutate over time: each message processing is purely functional, but
at the end the actor gets to pick what state it wants to mutate to before processing the
next message. This is an interesting method for mixing a bit of imperative programming
into a pure functional model.
In AF♭V you must write your own explicit message dispatch code using the Match
syntax of F♭V. We are here taking the dual approach to encoding to objects compared
to the objects-as-records approach we used in F♭OB, here taking objects as functions
and messages as variants. Lastly, in order to allow actors to know their own name, at
creation time it is passed to them.
7.2.2 An Example
Before getting into the operational semantics lets do a simple example. Here is an actor
that gets a start message and then counts down from its initial value to 0. Here, we
use the term Y to represent the Y-combinator and the term to represent a value of no
importance (as with the empty record {} before).
Here is a code fragment that another actor could use to fire up a new actor with the
above behavior and get it started. Suppose the above code we abbreviated CountTenBeh.
Here is an alternative way to count down, where the localdata field holds the value,
and its not in the message.
The latter example is the correct way to give actors local data – in the former example
the counter value had to be forwarded along every message.
Here is another usage fragment for the first example:
Let x = create(CountTenBeh,_)
In x <- ‘main(10); x <- ‘main(5)
In this case the actor x will in parallel and independently counting down from 10 .. 0
and 5 .. 0 - these counts may also interleave in random ways. For the second example
an analogue might be:
Let x = create(CountTenBeh2,10)
In x <- ‘count(_); x <- ‘count(_)
This does nothing but get one more count message queued up; since the actor sends a
new one out each time it gets one until 0, the effect will be to have a leftover message at
the end.
CHAPTER 7. CONCURRENCY 135
Actor systems can run forever, there is no notion of a final value. So, the system does
small steps of computation:
G1 → G2 → G3 → . . .
– this indicates one step of computation; in each single step one actor in the soup
competely processed one message in the soup. We will define this → relation below. →∗
is the reflexive, transitive closure of → – many steps of actor computation. In general
this continues infinitely since actor systems may not terminate. The final meaning of a
run of an actor system is this infinite stream of states.
(Value)
{}
v =⇒ v
The + rule suffices to show how the existing operator rules of F♭V must be tweaked
to add the potential for side effects S:
S S′
e1 =⇒ v1 , e2 =⇒ v2 where v1 , v2 ∈ Z
(+)
S∪S ′
e1 + e2 =⇒ the integer sum of v1 and v2
CHAPTER 7. CONCURRENCY 136
Since e or e′ above could in theory have each created actors or sent messages, we
need to append their effects to the final result. These effects are like state, they are on
the side. A major difference with F♭S is the effects here are ”write only” – they don’t
change the direction of local computation in any way, they are only spit out. In that
sense local actor computation stays functional.
All of the other pure Fb rules similarly union up the sets of messages/actors. Here
are a few more example rules in case it is not completely clear.
S S′
e1 =⇒ True e2 =⇒ v2
(If True)
S∪S ′
If e1 Then e2 Else e3 =⇒ v2
S S′
e1 =⇒ Fun x -> e, e2 =⇒ v2 , e[v2 /x] ⇒ v
(Application)
S∪S ′
e1 e2 =⇒ v
Now let us look at the more interesting rules, the rules that perform actor operations.
Here is the send rule:
S S′
e1 =⇒ a, e2 =⇒ v
(Send Rule)
S∪S ′ ∪{[a v]}
e1 e2 =⇒ v
The main consequence is the message [a v] is added to the soup. (The return result
v here is largely irrelevant, the goal of a message send is to add the side effect.)
Lastly, here is the create rule:
S S′ S ′′
e1 =⇒ v1 , e2 =⇒ v2 , v1 a v2 =⇒ v3
(Create Rule)
S∪S ′ ∪S ′′ ∪{⟨a,v3 ⟩}
Create(e1 , e2 ) =⇒ a, for a a fresh actor name
This time the return result matters - it is the name of the new actor. The running
of v1 a v2 passes the actor its own name and its initial values to initialize it, and so v1
will need to be a curried function of two arguments to accept these parameters a and v2
(in fact it needs to be a curried function of three arguments, because later the message
will also be passed as a parameter; more on that very soon).
S
(G ∪ {[a v ′ ]} ∪ {⟨a, v⟩}) → (G ∪ {⟨a, v ′′ ⟩} ∪ S) if (v v ′ =⇒ v ′′ )
This is the only global rule. It matches an actor with a message in the global soup that
is destined for it, uses the local semantics to run that actor (in isolation), and throws
back into the soup all of the S, which contains all the messages sent by this one actor run
as well as any new actors created by this one actor run. A global actor run is just the
repeated application of this rule. Notice how the actor behavior which was v is changed
to v ′′ , the result of this run.
To test these rules you can run the example programs above.
Compilation by Program
Transformation
The goal of this chapter is to understand the core concepts behind compilation by writing
a F♭SR compiler. Compilers are an important technology because code produced by a
compiler is faster than interpreted code by several orders of magnitude. At least 95% of
the production software running is compiled code. Compilation today is a very complex
process: compilers make multiple passes on a program to get source code to target code,
and perform many complex optimizing transformations. Our goals in this chapter are to
understand the most basic concepts behind compilation: how a high-level program can
be mapped to machine code.
We will outline a compiler of F♭SR to a very limited subset of C (“pseudo-assembly”).
The reader should be able to implement this compiler in Caml by filling in the holes we
have left out. The compiler uses a series of program transformations to express the
compilation process. These program transformations map F♭SR programs to equivalent
F♭SR programs, removing high-level features one at a time. In particular the following
transformations are performed in turn on a F♭SR program by our compiler:
1. Closure conversion
2. A-translation
3. Function hoisting
After a program has gone through these transformations, we have a F♭SR program that
is getting close to the structure of machine language. The last step is then the translate
of this primitive F♭SR program to C.
Real production compilers such as gcc and Sun’s javac do not use a transformation
process, primarily because the speed of the compilation itself is too slow. It is in fact
possible to produce very good code by transformation. The SML/NJ ML compiler uses a
transformational approach [5]. Also, most production compilers transform the program
to an intermediate form which is neither source nor target language (“intermediate lan-
guage”) and do numerous optimizing transformations on this intermediate code. Several
textbooks cover compiler technology in detail [6, 4].
138
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 139
Our main goal, unlike a production compiler, is understanding: to appreciate the gap
between high- and low-level code, and how the gaps may be bridged. Each transformation
that we define bridges one gap. Program transformations are interesting in their own
right, as they give insights into the F♭SR language. Optimization, although a central
topic in compilation, is beyond the scope of this book. Our focus is on the compilation
of higher-order languages, not C/C++; some of the issues are the same but others are
different. Also, our executables will not try to catch run-time type errors or garbage
collect unused memory.
The desired soundness property for each F♭SR program translation is: programs
before and after translation have the same execution behavior (in our case, termination
and same numerical output, but in general the same I/O behavior). Note that the
programs that are output by the translation are not necessarily operationally equivalent
to the originals.
The F♭SR transformations are now covered in the order they are applied to the
source program.
In the body x + y of the inner Fun y, x is a nonlocal and y is a local variable for that
function.
Now, we ask the question, what should add 3 return? Let us consider some obvious
choices:
• Fun y -> x + y wouldn’t make sense because the variable x would be undefined,
we don’t know its value is 3.
• Fun y -> 3 + y seems like the right thing, but it amounts to code substitution,
something a compiler can’t do since compiled code must be immutable.
Since neither of these ideas work, something new is needed. The solution is to return
a closure, a pair consisting of the function and an environment which remembers the
values of any nonlocal variables for later use:
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 140
Function definitions are now closure definitions; to invoke such a function a new pro-
cess is needed. Closure conversion is a global program transformation that explicitly
performs this operation in the language itself. Fun values are defined to be closures,
i.e. tuples of the function and an environment remembering the values of nonlocal vari-
ables. When invoking a function which is defined as a closure, we must explicitly pass
it the nonlocals environment which is in the closure so it can be used find values of the
nonlocals.
The translation is introduced by way of example. Consider the inner Fun y -> x +
y in add above translates to the closure
Let us look at the details of the translation. Closures are defined as tuples in the form
of records
consisting of the original function (the fn field) and the nonlocals environment (the envt
field), which is itself a record. In the nonlocals environment { x = [Link] }, x was a
nonlocal variable in the original function, and its value is remembered in this record using
a label of the same name, x. All such nonlocal variables are placed in the environment;
in this example x is the only nonlocal variable.
Functions that used to take an argument y are modified to take an argument named
yy (the original variable name, doubled up). We don’t really have to change the name
but it helps in understanding because the role of the variable has changed: the new
argument yy is expected to be a record of the form { envt = ..; arg = ..}, passing
both the environment and the original argument to the function.
If yy is indeed such a record at function invocation, then within the body we can use
[Link].x to access what was a nonlocal variable x in the original function body, and
[Link] to access what was the argument y to the function.
The whole add function is closure-converted by converting both functions:
add’ = {
fn = Fun xx -> {
fn = Fun yy -> ([Link].x) + ([Link]);
envt = { x = [Link] }
};
envt = {}
}
The outer Fun x -> ... arguably didn’t need to be closure-converted since it had no
nonlocals, but for uniformity it is best to closure convert all functions.
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 141
So, we first pull out the function part of the closure, (add’.fn), and then pass it a
record consisting of the environment add’.envt also pulled from the closure, and the
argument, 3. Translation of add 3 4 takes the result of the above, which should evaluate
to a function closure { fn = ...; envt = ...}, and does the same trick to apply 4 to
it:
and the result would be 12, the same as the original result, confirming the soundness of
the translation in this case. In general applications are converted as follows. At function
call time, the remembered environment in the closure is passed to the function in the
closure. Thus, for the add’ 3 closure above, add3’, when it is applied later to e.g. 7,
the envt will know it is 3 that is to be added to 7.
One more level of nesting Closure conversion is even slightly more complicated if
we consider one more level of nesting of function definitions, for example
The Fun z needs to get x, and since that Fun z is defined inside Fun y, Fun y has to be
an intermediary to pass from the outermost function x. Here is the translation.
triadd’ = {
fn = Fun xx -> {
fn = Fun yy -> {
fn = Fun zz ->
([Link].x) + ([Link].x) + ([Link]);
envt = { x = [Link].x; y = [Link] }
};
envt = { x = [Link] }
};
envt = {}
}
Some observations can be made. The inner z function has nonlocals x and y so both
of them need to be in its environment; The y function doesn’t directly use nonlocals, but
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 142
it has nonlocal x because the function inside it, Fun z, needs x. So its nonlocals envt
has x in it. Fun z can get x into its environment from y’s environment, as [Link].x.
Thus, Fun y serves as middleman to get x to Fun z.
1. clconv(x) = x (* variables *)
2. clconv(n) = n (* numbers *)
3. clconv(b) = b (* booleans *)
6. clconv(e op e’) = clconv(e) op clconv(e’) for all other operators in the lan-
guage (the translation is homomorphic in all of the other operators). This is pretty
clear in every case except maybe records which we will give just to be sure. . .
For the above example, clconv(add) is add’. The desired soundness result is
8.2 A-Translation
Machine language programs are linear sequences of atomic instructions; at most one
arithmetic operation is possible per instruction, so many instructions are needed to evalu-
ate complex arithmetic (and other) expressions. The A-translation closes the gap between
expression-based programs and linear, atomic instructions, by rephrasing expression-
based programs as a sequence of atomic operations. We represent this as a sequence of
Let statements, each of which performs one atomic operation.
The idea should be self-evident from the case of arithmetic expressions. Consider for
instance
4 + (2 * (3 + 2))
Our F♭SR interpreter defined a tree-notion of evaluation order on such expressions. The
order in which evaluation happens on this program can be made explicitly linear by using
Let to factor out the parts in the order that the interpreter evaluates the program
Let v1 = 3 + 2 In
Let v2 = 2 * v1 In
Let v3 = 4 + v2 In
v3
This program should give the same result as the original since all we did was to make
the computation sequence more self-evident. Notice how similar this is to 3-address
machine code: it is a linear sequence of atomic operations directly applied to variables
or constants. The v1 etc variables are temporaries; in machine code they generally
end up being assigned to registers. These temporaries are not re-used (re-assigned to)
above. Register-like programming is not possible in F♭SR but it is how real 3-address
intermediate language works. In the final machine code generation temporaries are re-
used (via a register allocation strategy).
We are in fact going to use a more naive (but uniform) translation, that also first
assigns constants and variables to other variables:
Let v1 = 4 In
Let v2 = 2 In
Let v3 = 3 In
Let v4 = 2 In
Let v5 = v3 + v4 In
Let v6 = v2 * v5 In
Let v7 = v1 + v6 In
v7
Exercise 8.1. Write out the operational semantics derivation and compare its structure
to the above.
This translation has the advantage that every operation will be between variables.
In the previous example above, 4+v2 may not be low-level enough for some machine
languages since there may be no add immediate instruction. One simple optimization
would be to avoid making fresh variables for constants. Our emphasis at this point is
on correctness as opposed to efficiency, however. Let is a primitive in F♭SR—this is
not strictly necessary, but if Let were defined in terms of application, the A-translation
results would be harder to manipulate.
Next consider F♭SR code that uses higher-order functions.
The function to which 2 is being applied first needs to be computed. We can make this
explicit via Let as well:
The full A-translation will, as with the arithmetic example, do a full linearization of all
operations:
Let v1 =
(Fun x ->
Let v1’ = (Fun y -> Let v1’’ = y in v1’’) In v1’)
In
Let v2 = 4 In
Let v3 = v1 v2 In
Let v4 = 2 In
Let v5 = v3 v4 In
v5
All forms of F♭SR expression can be linearized in similar fashion, except If:
If (3 = x + 2) Then 3 Else 2 * x
Let v1 = x + 2 In
Let v2 = (3 = v1) In
If v2 Then 3 Else Let v1 = 2 * x In v1
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 145
but the If still has a branch in it which cannot be linearized. Branches in machine code
can be linearized via labels and jumps, a form of expression lacking in F♭SR. The above
transformed example is still “close enough” to machine code: we can implement it as
v1 := x + 2
v2 := 3 = v1
BRANCH v2, L2
L1: v3 := 3
GOTO L3
L2: v4 := 4
L3:
but is a form easier to manipulate in Caml since lists of declarations will be appended
together at translation time. When writing a compiler, the programmer may or may not
want to use this intermediate form. It is not much harder to write the functions to work
directly on the Let representation.
We now sketch the translation for the core primitives. Assume the following auxiliary
functions have been defined:
• The function letize which converts from the list-of-tuples form to the actual Let
form, and
• resultId, which for list [(v1,e1); ...; (vn,en)] returns result identifier vn.
(* ... *)
At the end of the A-translation, the code is all “linear” in the way it runs in the
interpreter, not as a tree. Machine code is also linearly ordered; we are getting much
closer to machine code.
Theorem 8.2. A-translation is sound, i.e. e and atrans(e) both either compute to
values or both diverge.
Although we have only partially defined A-translation, the extra syntax of F♭SR
(records, reference cells) does not provide any major complication.
After these two phases, functions will have no nonlocal variables. Thus, we can hoist
all functions in the program body to the start of the program. This brings the program
structure more in line with C (and machine) code. Since our final target is C, the leftover
code from which all functions were hoisted then is made the main function. A function
hoist carries out this transformation. Informally, the operation is quite simple: take
e.g.
and replace it by
In general, we hoist all functions to the front of the code and give them a name via
Let. The transformation is always sound if there are no free variables in the function
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 147
body, a property guaranteed by closure conversion. We will define this process in a simple
iterative (but inefficient) manner:
let hoist e =
if e = e1[(Fun ea -> e’)/f] for some e1 with f free,
and e’ itself contains no functions
(i.e. Fun ea -> e’ is an innermost function)
then
Let f = (Fun ea -> e’) In hoist(e1)
else e
This function hoists out innermost functions first. If functions are not hoisted out
innermost-first, there will still be some nested functions in the hoisted definitions. So,
the order of hoisting is important.
The definition of hoisting given above is concise, but it is too inefficient. A one-pass
implementation can be used that recursively replaces functions with variables and accu-
mulates them in a list. This implementation is left as an exercise. Resulting programs
will be of the form
Lemma 8.1.
e1 [(Fun x -> e′ )/f ] ∼
=
(Let f = (Fun x -> e′ ) In e1
So, the program is almost nothing but a collection of functions, with a body that just
invokes main. This brings the program closer to C programs, which are nothing but a
collection of functions and main is implicitly invoked at program start.
Let Rec definitions also need to be hoisted to the top level; their treatment is similar
and will be left as an exercise.
8.4 Translation to C
We are now ready to translate into C. To summarize up to now, we have
We have done about all the translation that is possible within F♭SR. Programs are indeed
looking a lot more like machine code: all functions are declared at the top level, and each
function body consists of a linear sequence of atomic instructions (with exception of If
which is a branch). There still are a few things that are more complex than machine
code: records are still implicitly allocated, and function call is atomic, no pushing of
parameters is needed. Since C has function call built in, records are the only significant
gap that needs to be closed.
The translation involves two main operations.
2. For each function body, map each atomic tuple to a primitive C statement.
Atomic Tuples Before giving the translation, we enumerate all possible right-hand
sides of Let variable assignments that come out of the A-translation (in the following
vi, vj, vk, and f are variables).These are called the atomic tuples.
Fact 8.1 (Atomic Tuples). F♭SR programs that have passed through the first three phases
have function bodies consisting of tuple lists where each tuple is of one of the following
forms only:
1. x for variable x
2. n for number n
3. b for boolean b
4. vi vj (application)
5. vj + vk
6. vj - vk
7. vj And vk
8. vj Or vk
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 149
9. Not vj
10. vj = vk
11. Ref vj
12. vj := vk
13. !vj
15. vi.l
16. If vi Then tuples1 Else tuples2 where tuples1 and tuples2 are the lists of
variable assignments for the Then and Else bodies.
Functions should have all been hoisted to the top so there will be none of those in
the tuples. Observe that some of the records usages are from the original program, and
others were added by the closure conversion process. We can view all of them as regular
records. All we need to do now is generate code for each of the above tuples.
• On the run-time stack in the function’s activation record. The value is then refer-
enced as the memory location at some fixed offset from the stack pointer (which is
itself in a register).
Figure 8.1 illustrates the overall model of memory we are dealing with.
To elaborate a bit more on stack storage of variables, here is some C pseudo-code to
give you the idea of how the stack pointer sp is used.
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 150
r1 r2 r3
r4 r5 r6
Fixed Memory
CPU Registers
ActRec_n
ActRec_1
Memory
123
0x4C
Fixed Memory
CPU Registers
ActRec_n
{x = 5; y = 10}
0x4C..0x4F
ActRec_1
Memory
Figure 8.2: Boxed vs. unboxed values. The integer value 123 is stored as an unboxed
value, while the record {x=5; y=10} is stored as a boxed value.
Stack-stored entities are also temporary in that they will be junk when the function/method
returns.
Another important issue is whether to box or unbox values.
Figure 8.2 illustrates the difference between boxed and unboxed values.
For multi-word entities such as arrays, storing them unboxed means variables directly
hold a pointer to the first word of the sequence of space. To clarify the above concepts
we review C’s memory layout convention. Variables may be declared either as globals,
register (the register directive is a request to put in a register only), or on the call stack;
all variables declared inside a function are kept on the stack. Variables directly holding
ints, floats, structs, and arrays are all unboxed. (Examples: int x; float x; int
arr[10]; snork x for snork a struct.) There is no such thing as a variable directly
holding a function; variables in C may only hold pointers to functions. It is possible to
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 152
write “v = f” in C where f is a previously declared function and not “v = &f”, but that
is because the former is really syntactic sugar for the latter. A pointer to a function is in
fact a pointer to the start of the code of the function. Boxed variables in C are declared
explicitly, as pointer variables. (Examples: int *x; float *x; int *arr[10]; snork
*x for snork a struct.) All malloc’ed structures must be stored in a pointer variable
because they are boxed: variables can’t directly be heap entities. Variables are static
and the heap is dynamic.
Here is an example of a simple C program and the Sun SPARC assembly output
which gives some impressionistic idea of these concepts:
int glob;
main()
{
int x;
register int reg;
int* mall;
int arr[10];
x = glob + 1;
reg = x;
mall = (int *) malloc(1);
x = *mall;
arr[2] = 4;
/* arr = arr2; --illegal: arrays are not boxed */
}
In the assembly language, %o1 is a register, [%o0] means dereference, [%fp-24] means
subtract 24 from frame pointer register %fp and dereference. The assembly representation
of the above C code is as follows.
main:
sethi %hi(glob), %o1
or %o1, %lo(glob), %o0 /* load global address glob into %o0 */
ld [%o0], %o1 /* dereference */
add %o1, 1, %o0 /* increment */
st %o0, [%fp-20] /* store in [%fp-20], 20 back from fp -- x */
/* x directly contains a number,
/* not a pointer */
ld [%fp-20], %l0 /* %l0 IS reg (its in a register directly) */
mov 1, %o0
call malloc, 0 /* call malloc. resulting address to %o0 */
nop
st %o0, [%fp-24] /* put newspace location in mall [%fp-24] */
ld [%fp-24], %o0 /* load mall into %o0 */
ld [%o0], %o1 /* this is a malloced structure -- unbox. */
st %o1, [%fp-20] /* store into x */
mov 4, %o0
st %o0, [%fp-56] /* array is a sequence of memory on stack */
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 153
.LL2:
ret
restore
Our memory layout strategy is more like a higher-level language such as Java or ML.
The Java JVM uses a particular, fixed, memory layout scheme: all object references are
boxed pointers to heap locations; the primitive bool, byte, char, short, int, long,
float, and double types are kept unboxed. Since Java arrays are objects they are
also kept boxed. There is no reference (&) or dereference (*) operator in Java. These
operations occur implicitly.
Memory layout for our F♭SR compiler F♭SR is (mostly) a Caml subset and so its
memory is also managed more implicitly than C memory. We will use a simple, uniform
scheme in our compilers which is close in spirit to Java’s: Box Refs and records and
function values, but keep boolean values and integers unboxed. Also, as in C (and Java),
all local function variables will be allocated on the stack and accessed as offsets from
the stack pointer. We will achieve the latter by implementing F♭SR local variables as C
local variables, which will be stack allocated by the C compiler.
Since a Ref is nothing but a mutable location, there may not seem to be any reason
to box it. However, if a function returns a Ref as result, and it were not boxed, it would
have been allocated on the stack and thus would be deallocated. Here is an example that
reflects this problem:
If Ref 5 were stored on the stack, after the return it could be wiped out. All of the
Let-defined entities in our tuples (the vi variables) can be either in registers or on the
call stack: none of those variables are directly used outside the function due to lexical
scoping, and they don’t directly contain values that should stay alive after the function
returns. For efficiency, they can all be declared as register Word variables:
One other advantage of this simple scheme is every variable holds one word of data,
and thus we don’t need to keep track of how much data a variable is holding. This
scheme is not very efficient, and real compilers optimize significantly. One example is
Ref’s which are known to not escape a function can unboxed and stack allocated.
All that remains is to come up with a scheme to compile each of the above atomic
tuples and we are done. Records are the most difficult so we will consider them before
writing out the full translation.
Compiling untyped records Recall from when we covered records that the fields
present in a record cannot be known in advance if there is no type system. So, we won’t
know where the field that we need is exactly. Consider, for example,
Field l will be in two different positions in these records so the selection will not have
a sole place it can find the field in. Thus we will need to use a hashtable for record
lookup. In a typed language such as Caml this problem is avoided: the above code is
not well-typed in Caml because the if-then can’t be typed. Note that the problems with
records are closely related to problems with objects, since objects are simply records with
Refs.
This memory layout difficulty with records illustrates an important relationship be-
tween typing and compilation. Type systems impose constraints on program structure
that can make compilers easier to implement. Additionally, typecheckers will obviate
the need to deal with certain run-time errors. Our simple F♭SR compilers are going to
core dump on e.g. 4 (5); in Lisp, Smalltalk, or Scheme these errors would be caught
at run-time but would slow down execution. In a typed language, the compiler would
reject the program since it will not typecheck. Thus for typed languages they will both
be faster and safer.
Our method for compilation of records proceeds as follows. We must give records
a heavy implementation, as hash tables (i.e., a set of key-value pairs, where the keys
are label names). In order to make the implementation simple, records are boxed so
they take one word of memory, as mentioned above when we covered boxing. A record
selection operation vk .l is implemented by hashing on key l in the hash table pointed to
by vk at runtime. This is more or less how Smalltalk message sends are implemented,
since records are similar to objects (and Smalltalk is untyped).
The above is less than optimal because space will be needed for the hashtable, and
record field accessing will be much slower than, for example, struct access in C. Since
closures are records, this will also significantly slow down function call. A simple opti-
mization would be to treat closure records specially since the field positions will always
be fixed, and use a struct implementation of closure (create a different struct type for
each function).
For instance, consider
toCFunctions([]) = ""
toCFunctions(Functiontuple::Functiontuples) =
toCFunction(Functiontuple) ^ toCFunctions(Functiontuples)
The reader may wonder why a fresh memory location is allocated for a Ref, as opposed
to simply storing the existing address of the object being referenced. This is a subtle
issue, but the code vi = &vj, for example, would definitely not work for the Ref case
(vj may go out of scope).
This translation sketch above leaves out many details. Here is some elaboration.
Typing issues We designed out memory layout so that every entity takes up one word.
So, every variable is of some type that is one word in size. Type all variables as Word’s,
where Word is a 1-word type (defined as e.g. typedef void *Word;). Many type casts
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 156
need to be inserted; we are basically turning off the type-checking of C, but there is no
”switch” that can be flicked. So, for instance vi = vj + vk will really be vi = (Word
(int vj) + (int vk)) – cast the words to ints, do the addition, and cast back to
a word. To cast to a function pointer is a tongue-twister: in C you can use (*((Word
(*)()) f))(arg). The simplest way to avoid confusion when actually writing a compiler
is to include the following typedefs to the resulting C code:
/*
* Define the type ‘Word’ to be a generic one-word value.
*/
typedef void *Word;
/*
* Define the type ‘FPtr’ to be a pointer to a function that
* consumes Word and returns a Word. All translated
* functions should be of this form.
*/
typedef Word (*FPtr)(Word);
/*
* Here is an example of how to use these typedefs in code.
*/
Word my function(Word w) {
return w;
}
int main(int argc, char **argv) {
Word f1 = (Word) my function;
Word w1 = (Word) 123;
Word w2 = ( *((FPtr) f1) )(w1); /* Computes f1(123) */
printf("%d\n", (int) w2); /* output is "123\n". */
return 0;
}
Global Issues Some global issues you will need to deal with include the following.
You will need to print out the result returned by the main function (so, you probably
want the FbSR main function to be called something like FbSRmain and then write your
own main() by hand which will call FbSRmain); The C functions need to declare all the
temporary variables they use. One solution is to declare in the function header a C array
Word v[22]
where 22 is the number of temporaries needed in this particular function, and use names
v[0], v[1], etc for the temporaries. Note, this works well only if the newid() function
is instructed to start numbering temporaries at zero again upon compiling each new
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 157
function. Every compiled program is going to have to come with a standard block of
C code in the header, including the record hash implementation, main() as alluded to
above, etc.
Other issues that present problems include the following. Record creation is only
sketched; but there are many C hash set libraries that could be used for this purpose.
The final result (resultId) of the Then and Else tuples needs to be in the same variable
vi, which is also the variable where the result of the tuple is put, for the If code to be
correct. This is best handled in the A-translation phase.
There are several other issues that will arise when writing the compiler. The full
implementation is left as an exercise.
8.5 Summary
let frontend e = hoist(atrans(clconv(e)));;
let translator e = toC(frontend(e));;
We can assert the correctness of our translator.
Theorem 8.4. FbSR program e terminates in the F♭SR operational semantics (or eval-
uator) just when the C program translator(e) terminates, provided the C program
does not run out of memory. Core dump or other run-time errors are equated with
nontermination. Furthermore, if F♭SR’s eval(e) returns a number n, the compiled
translator(e) will also produce numerical output n.
8.6 Optimization
Optimization can be done at all phases of the translation process. The above translation
is simple, but inefficient. There is always a tradeoff between simplicity and efficiency in
compiler designs, both the efficiency of compilation itself, and efficiency of code produced.
In the phases before C code is produced, optimizations consist of replacing chunks of the
program with operationally equivalent chunks.
Some simple optimizations include the following. The special closure records {fn =
.., envt = .. } could be implemented as a pointer to a C struct with fn and envt
fields, instead of using the very slow hash method,1 will significantly speed up the code
produced. Records which do not not have field names overlapping with other records
can also be implemented in this manner (there can be two different records with the
1
Although hash lookups are O(1), there is still a large amount of constant overhead, whereas struct
access can be done in a single load operation.
CHAPTER 8. COMPILATION BY PROGRAM TRANSFORMATION 158
same fields, but not two different records with some fields the same and some different).
Another optimization is to modify the A-translation to avoid making tuples for variables
and constants. Constant expressions such as 3 + 4 can be folded to 7.
More fancy optimizations require a global flow analysis be performed. Simply put,
a flow analysis finds all possible uses of a particular definition, and all possible definitions
corresponding to a particular use.
A definition is a record, a Fun, or a number or boolean, and a use is a record field
selection, function application, or numerical or boolean operator.
[4] A.V. Aho, R. Sethi, and J.D. Ullman. Compilers: Principles, Techniques and Tools.
Addison-Wesley, 1986.
[7] Richard Bird. Introduction to Functional Programming using Haskell. Prentice Hall,
2nd edition, 1998.
[9] Kim Bruce. Foundations of Object-Oriented Languages: Types and Semantics. MIT
Press, 2002.
[10] Jonathan Eifrig, Scott Smith, Valery Trifonov, and Amy Zwarico. Application of
OOP type theory: State, decidability, integration. In OOPSLA ’94, pages 16–30,
1994.
[11] Martin Fowler. UML Distilled. Addison Wesley, 2nd edition, 2000.
[12] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns:
Elements of Reusable Object-Oriented Software. Addison-Wesley Professional Com-
puting Series. Addison-Wesley, 1994.
[13] Paul Hudak, John Peterson, and Joseph Fasel. A gentle introduction to Haskell,
version 98, June 2000. [Link]
159
BIBLIOGRAPHY 160
[15] Xavier Leroy. The Objective Caml system release 3.11, documentation and
user’s manual, November 2008. [Link]
[Link].
[16] Ian A. Mason, Scott F. Smith, and Carolyn L. Talcott. From operational semantics
to domain theory. Information and Computation, 128(1):26–47, 1996.
[17] Greg Morrisett, Karl Crary, Neal Glew, Dan Grossman, Richard Samuels, Frederick
Smith, David Walker, Stephanie Weirich, and Steve Zdancewic. Talx86: A realistic
typed assembly language. In 1999 ACM SIGPLAN Workshop on Compiler Support
for System Software, pages 25–35, Atlanta, GA, USA, May 1999.
[18] J J O’Connor and E F Robertson. Gottfried Wilhelm von Leibniz. The MacTu-
tor History of Mathematics Archive, October 1998. [Link]
[Link]/history/Mathematicians/[Link].
[19] Randall B. Smith and David Ungar. Self: The power of simplicity. In Conference
proceedings on Object-oriented programming systems, languages and applications,
pages 227–242. ACM Press, 1987.
[20] Randall B. Smith and David Ungar. Programming as an experience: The inspiration
for Self. Lecture Notes in Computer Science, 952:303–??, 1995.
[23] Paul R. Wilson. Uniprocessor garbage collection techniques. ACM Computing Sur-
veys, 2002. [Link]
Index
161
INDEX 162