Introduction To Theory of Programming Languages
Introduction To Theory of Programming Languages
Introduction to
Programming Languages
Anthony A. Aaby
DRAFT Version 0.9. Edited July 15, 2004
Copyright
c 1992-2004 by Anthony A. Aaby
Walla Walla College
204 S. College Ave.
College Place, WA 99324
E-mail: [email protected]
This work is licensed under the Creative Commons Attribution License. To view
a copy of this license, visit http://creativecommons.org/licenses/by/2.0/ or send
a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California
94305, USA.
This book is distributed in the hope it will be useful, but without any warranty;
without even the implied warranty of merchantability or fitness for a particular
purpose.
No explicit permission is required from the author for reproduction of this book
in any medium, physical or electronic.
The author solicits collaboration with others on the elaboration and extension
of the material in this text. Users are invited to suggest and contribute material
for inclusion in future versions provided it is offered under compatible copyright
provisions. The most current version of this text and LATEXsource is available
at http://www.cs.wwc.edu/~aabyan/Logic/index.html.
To Ogden and Amy Aaby
iv
Preface
v
Special Features of the Text
The following are significant features of the text as compared to the standard
texts.
Readership
This book is intended as an undergraduate text in the theory of programming
languages. To gain maximum benefit from the text, the reader should have ex-
perience in a high-level programming language such as Pascal, Modula-2, C++,
ML or Common Lisp, machine organization and programming, and discrete
mathematics.
Programming is not a spectator sport. To gain maximum benefit from the text,
the reader should construct programs in each of the paradigms, write semantic
specifications; and implement a small programming language.
Organization
Since the subject area PL: Programming Languages as described in the “ACM/IEEE
Computing Curricula 1991” consists of a minimum of 47 hours of lecture, the
text contains considerably more material than can be covered in a single course.
vi
The first part of the text consists of chapters 1–3. Chapter 1 is an overview
of the text, an introduction to the areas of discussion. It introduces the key
concepts: the models of computation, syntax, semantics, abstraction, general-
ization and pragmatics which are elaborated in the rest of the text. Chapter
2 introduces context-free grammars, regular expressions, and attribute gram-
mars. Context-free grammars are utilized throughout the text but the other
sections are optional. Chapter 3 introduces semantics: algebraic, axiomatic,
denotational and operational. While the chapter is optional, I introduce al-
gebraic semantics in conjunction with abstract types and axiomatic semantics
with imperative programming.
Chapter 4 is a formal treatment of abstraction and generalization as used in
programming languages.
Chapter 5 deals with values, types, type constructors and type systems. Chapter
6 deals with environments, block structure and scope rules. Chapter 7 deals
with the functional model of computation. It introduces the lambda calculus
and examines Scheme and Haskell. Chapter 8 deals with the logic model of
computation. It introduces Horn clause logic, resolution and unification and
examines Prolog. Chapter 9 deals with the imperative model of computation.
Features of several imperative programming languages are examined. Various
parameter passing mechanisms should be discussed in conjunction with this
chapter. Chapter 10 deals with the concurrent model of programming. Its
primary emphasis is from the imperative point of view. Chapter 11 is a further
elaboration of the concepts of abstraction and generalization in the module
concept. It is preparatory for Chapter 12. Chapter 12 deals with the object-
oriented model of programming. Its primary emphasis is from the imperative
point of view. Features of Smalltalk, C++ and Modula-3 provide examples.
Chapter 13 deals with pragmatic issues and implementation details. It may be
read in conjunction with earlier chapters. Chapter 14 deals with programming
environments, Chapter 15 deals with the evaluation of programming languages
and a review of programming language design principles. Chapter 16 contains
a short history of programming languages.
Pedagogy
The text provides pedagogical support through various exercises and labora-
tory projects. Some of the projects are suitable for small group assignments.
The exercises include programming exercises in various programming languages.
Some are designed to give the student familiarity with a programming concept
such as modules, others require the student to construct an implementation of a
programming language concept. For the student to gain maximum benefit from
the text, the student should have access to a logic programming language (such
as Prolog), a modern functional language (such as Scheme, ML or Haskell),
a concurrent programming language (Ada, SR, or Occam), an object-oriented
programming language (C++, Small-Talk, Eiffel, or Modula-3), and a modern
vii
programming environment and programming tools. Free versions of Prolog,
ML, Haskell, SR, and Modula-3 are available from one or more ftp sites and are
recommended.
The instructor’s manual contains lecture outlines and illustrations from the text
which may be transferred to transparencies. There is also a laboratory manual
which provides short introductions to Lex, Yacc, Prolog, Haskell, Modula-3, and
SR.
The text has been used as a semester course with a weekly two hour lab. Its
approach reflects the core area of programming languages as described in the re-
port Computing as a Discipline in CACM January 1989 Volume 32 Number
1.
Acknowledgements
There are several programming texts that have influenced this work in partic-
ular, texts by Hehner, Tennent, Pratt, and Sethi. I am grateful to my CS208
classes at Bucknell for their comments on preliminary versions of this material
and to Bucknell University for providing the excellent environment in and with
which to develop this text.
AA 1992
viii
Contents
Preface v
1 Introduction 1
1.1 Models of Computation . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Syntax and Semantics . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3 Pragmatics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.4 Language Design Principles . . . . . . . . . . . . . . . . . . . . . 8
1.5 Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.6 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2 Syntax 13
2.1 Context-Free Grammars . . . . . . . . . . . . . . . . . . . . . . . 14
2.2 Regular Expressions . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.3 Attribute Grammars and Static Semantics . . . . . . . . . . . . . 23
2.4 Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.5 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3 Semantics 27
3.1 Algebraic Semantics . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.2 Axiomatic Semantics . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.3 Denotational Semantics . . . . . . . . . . . . . . . . . . . . . . . 36
3.4 Operational Semantics . . . . . . . . . . . . . . . . . . . . . . . . 37
3.5 Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
ix
4.2 Generalization . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
4.3 Substitution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.4 Abstraction and Generalization . . . . . . . . . . . . . . . . . . . 46
4.5 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
6 Environment 71
6.1 Block structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
6.2 Declarations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
6.3 Constants . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
6.4 User Defined Types . . . . . . . . . . . . . . . . . . . . . . . . . . 74
6.5 Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
6.6 Functions and Procedures . . . . . . . . . . . . . . . . . . . . . . 78
6.7 Persistant Types . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
6.8 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
7 Functional Programming 81
7.1 The Lambda Calculus . . . . . . . . . . . . . . . . . . . . . . . . 83
7.2 Recursive Functions . . . . . . . . . . . . . . . . . . . . . . . . . 88
7.3 Lexical Scope Rules . . . . . . . . . . . . . . . . . . . . . . . . . 90
7.4 Functional Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
7.5 Evaluation Order . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
7.6 Values and Types . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
7.7 Type Systems and Polymorphism . . . . . . . . . . . . . . . . . . 93
7.8 Program Transformation . . . . . . . . . . . . . . . . . . . . . . . 93
7.9 Pattern matching . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
x
7.10 Combinatorial Logic . . . . . . . . . . . . . . . . . . . . . . . . . 94
7.11 Scheme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
7.12 Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
7.13 Discussion and Further Reading . . . . . . . . . . . . . . . . . . . 100
7.14 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
xi
9.15 Expression-oriented languages . . . . . . . . . . . . . . . . . . . . 145
9.16 Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
11 PCN 161
11.1 Tutorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
11.2 The PCN Language . . . . . . . . . . . . . . . . . . . . . . . . . 161
11.3 Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
xii
13.6 Types and Classes . . . . . . . . . . . . . . . . . . . . . . . . . . 175
13.7 Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
13.8 Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
13.9 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
14 Pragmatics 179
14.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
14.2 Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
14.3 Bindings and Binding Times . . . . . . . . . . . . . . . . . . . . 180
14.4 Values and Types . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
14.5 Computational Models . . . . . . . . . . . . . . . . . . . . . . . . 182
14.6 Procedures and Functions . . . . . . . . . . . . . . . . . . . . . . 182
14.7 Scope and Blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
14.8 Parameters and Arguments . . . . . . . . . . . . . . . . . . . . . 187
14.9 Safety . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
14.10Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
14.11Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
15 Translation 191
15.1 Parsing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
15.2 Scanning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
15.3 The Symbol Table . . . . . . . . . . . . . . . . . . . . . . . . . . 193
15.4 Virtual Computers . . . . . . . . . . . . . . . . . . . . . . . . . . 193
15.5 Optimization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
15.6 Code Generation . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
15.7 Peephole Optimization . . . . . . . . . . . . . . . . . . . . . . . . 199
15.8 Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
17 History 207
xiii
17.1 Functional Programming . . . . . . . . . . . . . . . . . . . . . . . 207
17.2 Logic Programming . . . . . . . . . . . . . . . . . . . . . . . . . 208
17.3 Imperative Programming . . . . . . . . . . . . . . . . . . . . . . 208
17.4 Concurrent Programming . . . . . . . . . . . . . . . . . . . . . . 209
17.5 Object-Oriented Programming . . . . . . . . . . . . . . . . . . . 209
A Logic 211
A.1 Sentential Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
A.1.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
A.1.2 Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
A.2 Predicate Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
A.2.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
A.2.2 Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
xiv
Chapter 1
Introduction
Suppose that we have the values 3.14 and 5, the operation of multiplication (×)
and we perform the computation specified by the following arithmetic expression
3.14 × 5
the result of which is the value:
15.7
The value 3.14 is readily recognized as an approximation for π. The actual
numeric value may be less important than knowing that an approximation to π
is intended so we can replace 3.14 with π. abstracting the expression 3.14 × 5
to:
π × 5 where π = 3.14
We say that π is bound to 3.14 and is a constant. The “where” introduces a
local environment or block where additional definitions may occur.
1
2 CHAPTER 1. INTRODUCTION
The variable diameter appearing in the right hand side is no longer free. It is
bound to the parameter diameter. Circumference has a value (other than the
right hand side) only when the parameter is replaced with an expression. For
example,
Circumf erence(5) = 15.7
The parameter diameter is bound to the value 5 and, as a result, Circumfer-
ence(5) is bound to 15.7.
In this form, the definition is a recipe or program for computing the circum-
ference of a circle from the diameter of the circle. The mathematical notation
(syntax) provides the programming language and arithmetic provides the com-
putational model for the computation. The mapping from the syntax to the
computational model provides the meaning (semantics) for the program. The
notation employed in this example is based on the very pragmatic considera-
tions of ease of use and understanding. It is so similar to the usual mathematical
notation that it is difficult to distinguish between the notation and the compu-
tational model. This example serves to illustrate several key ideas in the study
of programming languages which are summarized in the following definitions:
Definition 1.1
Computational models begin with a set of values. The values can be separated
into two groups, primitive and composite. The primitive values (or types) are
usually numbers, boolean values, and characters. The composite values (or
types) are usually arrays, records, and recursively defined values. Strings may
occur as either primitive or composite values. Lists, stacks, trees, and queues are
examples of recursively defined values. Associated with the primitive values are
the usual operations (e.g., arithmetic operations for the numbers). Associated
with each composite type are operations to construct the values of that type
and operations to access component elements of the type.
f x = 2∗x + 3
4 CHAPTER 1. INTRODUCTION
sd xs = sqrt( (sumsqrs( xs ) -
(sum( xs )^2 / length( xs ) )) / length( xs ))
The functional model is important because it has been under development for
hundreds of years and its notation and methods form the base upon which a
large portion of our problem solving methodologies rest.
The logic model of computation is based on relations and logical inference. Pro-
grams consist of definitions of relations and computations are inferences. For
example the linear function y = 2x + 3 can be represented as:
f(X,Y) if Y is 2∗X + 3.
1. man(Socrates)
2. mortal(X) if man(X)
The first line is a translation of the statement Socrates is a man. The second
line is a translation of the phrase all men are mortal into the equivalent for all
X, if X is a man then X is mortal. To determine the mortality of Socrates. The
following sentence must be added to the set.
¬ mortal(Y)
This sentence is a translation of the phrase There are no mortals rather than
the usual phrase Socrates is not mortal. It can be viewed as the question, “Is
there a mortal?” The first step in the computation is illustrated here
1.1. MODELS OF COMPUTATION 5
1. man(Socrates)
2. mortal(X) if man(X)
3. ¬ mortal(Y)
4. ¬ man(Y)
The deduction of line 4 from lines 2 and 3 is to be understood from the fact that
if the conclusion of a rule is known to be false, then so is the hypothesis (modus
tollens). Using this new result, we get a contradiction with the first sentence.
1. man(Socrates)
2. mortal(X) if man(X)
3. ¬ mortal(Y)
4. ¬ man(Y)
5. Y = Socrates
From the resolvent and the first sentence, resolution and unification produce
Y=Socrates. That is, there is a mortal and one such mortal is Socrates. Res-
olution is the process of looking for a contradiction and it is facilitated by
unification which determines if there is a substitution which makes two terms
the same.
Y := 2∗X + 3
requires the implementation to determine the value of X in the state and then
create a new state which differs from the old state in that the value of Y in the
new state is the value that 2∗X + 3 had in the old state.
The imperative model is important because it models change and changes are
part and parcel of our environment. In addition, it is the closest to modeling
6 CHAPTER 1. INTRODUCTION
the hardware on which programs are executed. This tends to make it the most
efficient model in terms of execution speed.
Other Models
Computability
The notation used in the functional and logic models tends to reflect common
mathematical practice and thus, it tends toward simplicity and regularity. On
the other hand, the notation used for the imperative model tends to be irregu-
lar and of greater complexity. The problem is that in the imperative model the
programmer must both manage storage and determine the appropriate compu-
tations. This tends to permit programs that are more efficient in their use of
time and space than equivalent functional and logic programs. The addition of
concurrency to imperative programs results in additional syntactic structures
while concurrency in functional and logic programs is more of an implementation
issue.
The relationship between the syntax and the computational model is provided
by semantic descriptions. The semantics of imperative programming languages
tends to receive more attention because changes to state need not be restricted
to local values. In fact, the bulk of the work done in the area of programming
language semantics deals with imperative programming languages.
Since semantics ties together the syntax and the computational model, there are
several programming language design principles which are deal with the interac-
tion between these three areas. Since syntax is the means by which computation
is specified, the following programming language design principle deals with the
relationship which must exist between syntax and the compuational model.
1.3 Pragmatics
Programs are written and read by humans but are executed by computers.
Since both humans and computers must be able to understand programs, it is
necessary to understand the requirements of both classes of users.
Natural languages are not suitable for programming languages because humans
themselves do not use natural languages when they construct precise formula-
tions of concepts and principles of particular knowledge domains. Instead, they
use a mix of natural language and the formalized symbolic notations of math-
ematics and logic and various diagrams. The most successful of these symbolic
notations contain a few basic objects which may be combined through a few sim-
ple rules to produce objects of arbitrary levels of complexity. In these systems,
humans reduce complexity by the use of definitions, abstractions, generaliza-
tions and analogies. Benjamin Whorf[32] has postulated that one’s language
has considerable effect on the way that one thinks; indeed on what one can
think. This suggests that programming languages should cater to the natural
problem solving approaches used by humans. Miller[21] observes that people
can keep track of about seven things. This suggests that a programming lan-
guage should provide mechanisms which support abstraction and generalization.
Programming languages should approach the level at which humans reason and
should reflect the notational approaches that humans use in problem solving and
further must include ways of structuring programs to ease the tasks of program
understanding, debugging and maintenance.
Load X R1
Mult R1 2 R1
Add R1 3 R1
Store R1 Y
This example indicates that machine languages tend to be difficult for humans
to read and write.
The principle of regularity and and extensibility require that the basic concepts
of the language should be applied consistently and universally.
1.6 Exercises
1. Classify the following languages in terms of a computational model: Ada,
APL, BASIC, C, COBOL, FORTRAN, Haskell, Icon, LISP, Pascal, Pro-
log, SNOBOL.
2. For the following applications, determine an appropriate computational
model which might serve to provide a solution: automated teller machine,
flight-control system, a legal advice service, nuclear power station moni-
toring system, and an industrial robot.
3. Compare the syntactical form of the if-command/expression as found in
Ada, APL, BASIC, C, COBOL, FORTRAN, Haskell, Icon, LISP, Pascal,
Prolog, SNOBOL.
4. An extensible language is a language which can be extended after language
design time. Compare the extensibility features of C or Pascal with those
of LISP or Scheme.
5. What programming language constructs of C are dependent on the local
environment?
6. What languages provide for binding of type to a variable at run-time?
7. Discuss the advantages and disadvantages of early and late binding for the
following language features. The type of a variable, the size of an array,
the forms of expressions and commands.
8. Compare two programming languages from the same compuational paradigm
with respect to the programming language design principles.
12 CHAPTER 1. INTRODUCTION
Chapter 2
Syntax
Syntax is concerned with the appearance and structure of programs. The syn-
tactic elements of a programming language are largely determined by the com-
putation model and pragmatic concerns. There are well developed tools (reg-
ular, context-free and attribute grammars) for the description of the syntax of
programming languages. The grammars are rewriting rules and may be used
for both recognition and generation of programs. Grammars are independent
of computational models and are useful for the description of the structure of
languages in general.
This chapter provides an introduction to grammars and shows how they may be
used for the description of the syntax of programming languages. Context-free
grammars are used to describe the bulk of the language’s structure; regular ex-
pressions are used to describe the lexical units (tokens); attribute grammars are
used to describe the context sensitive portions of the language. Both concrete
and abstract grammars are presented.
13
14 CHAPTER 2. SYNTAX
Preliminary definitions
The following definitions are basic to the definition of regular expressions and
context-free grammars.
The alphabet for the lexical tokens of programming language is the character
set. The alphabet for the context-free structure of a programming language is
the set of keywords, identifiers, constants and delimiters; the lexical tokens.
Definition 2.4 Let Σ be an alphabet. The set of all possible finite strings of
elements of Σ is denoted by Σ∗ . The set of all possible nonempty strings of Σ
is denoted by Σ+ .
Concrete Syntax
N = {E}
T = {id, +, ∗, (, )}
P = {E → id, E → (E), E → E + E, E → E ∗ E}
S=E
E [id + id ∗ id]
E+E [id] + [id ∗ id]
id + E id + [id ∗ id]
id + E ∗ E id + [id] ∗ [id]
id + id ∗ E id + id ∗ [id]
id + id ∗ id id + id ∗ id
A → w where A ∈ N , w ∈ (N ∪ T )∗ , and S ∈ N .
Grammars may be used both for recognition and generation of strings. Recog-
nition and generation requires finding a rewriting sequence consisting of appli-
cations of the rewriting rules which begins with the grammar’s start symbol
and ends with the string in question. For example, a rewriting sequence for the
string
id + id ∗ id
is given in Figure 2.2. The first column is the rewriting sequence which
generates the string. The same rewriting sequence is used to recognize the
string in the second column. The bracketed portions of the string indicate the
unrecognized portions of the string and correspond to nonterminals in the first
column.
Ambiguity
E [id + id ∗ id]
E∗E [id + id] ∗ [id]
E+E∗E [id] + [id] ∗ [id]
id + E ∗ E id + [id] ∗ [id]
id + id ∗ E id + id ∗ [id]
id + id ∗ id id + id ∗ id
rules for the derivation of the string id + id ∗ id. Note that the derivation in
Figure 2.3 suggests that addition is performed before multiplication in violation
of standard arithmetic practice. In these examples we have chosen to rewrite the
left-most non-terminal first. When there are two or more left-most derivations
of a string in a given grammar, the grammar is said to be ambiguous. In some
instances ambiguity may be eliminated by the selection of another grammar for
the language or by establishing precedences among the operators.
Parsers
id + id ∗ id
using a top down parser and a bottom up parser for the expression grammar
of Figure 2.1. Figure 2.4 shows the state of the stack and input string during
the top down parse. Parsing begins with the start symbol on top of the stack.
2.1. CONTEXT-FREE GRAMMARS 17
Stack Input
E] id+id*id]
E+E] id+id*id]
id+E] id+id*id]
+E] +id*id]
E] id*id]
E*E] id*id]
id*E] id*id]
E] *id]
E] id]
id] id]
] ]
Since the top of the stack does not match the input, it is popped and the right
hand side of the production E → E + E is pushed onto the stack. Still a match
is not found so the parser pops the top of the stack and pushes the right hand
side of the production E → id onto the stack. Now the top of the stack matches
the input symbol so the top of the stack is popped and the input symbol is
consumed. The parser continues until both the stack and the input are empty
in which case the input is accepted or an error state occurs. The finite control
follows the following algorithm:
(a) If the top of the stack and the next input symbol are the same, pop
the top of the stack and consume the input symbol.
(b) If the top of the stack is a non-terminal symbol, pop the stack and
push the right hand side of the corresponding grammar rule onto the
stack.
3. If both the stack and input are empty, accept the input otherwise, reject
the input.
Figure 2.5 shows the state of the stack and input string of a bottom up parser
for the expression grammar of Figure 2.1. Parsing begins with an empty stack.
Since there are no symbols in the stack, the first symbol of the input is shifted
to the stack. The top of the stack then corresponds to the right hand side
of the production E → id so the stack is reduced to the left hand side of the
production by popping the stack and pushing the symbol E onto the stack. The
18 CHAPTER 2. SYNTAX
Stack Input
] id+id*id]
id] +id*id]
E] +id*id]
+E] id*id]
id+E] *id]
E+E] *id]
E+E] id]
id*E+E] ]
E*E+E] ]
E+E ]
E] ]
parse continues by a series of shifts and reductions until the start symbol is the
only symbol left on the stack and the input is empty in which case the input
is accepted or an error state occurs. The finite control follows the following
algorithm:
(a) If the top n stack symbols match the right hand side of a grammar
rule in reverse, then reduce the stack by replacing the n symbols with
the left hand symbol of the grammar rule.
(b) If no reduction is possible then shift the current input symbol to the
stack.
3. If the input is empty and the stack contains only the start symbol of the
grammar, then accept the input otherwise, reject the input.
In both these examples the choice of the which production to use appears to
be magical. In the case of a top down parser the grammar should be rewritten
to remove the ambiguity. Exercise 8 contains an appropriate grammar for a
top down parser for expressions. For bottom up parsers, there are techniques
for the analysis of the grammar to produce a set of unambiguous choices for
productions. Such techniques are beyond the scope of this text.
2.1. CONTEXT-FREE GRAMMARS 19
Backus-Naur Form
• Typeface: The names of BNF categories are written in italics and without
< and >.
• Zero or More: Zero or more repetitions of the previous syntactical unit
are indicated with ellipsis (...).
• Optional: Optional elements are enclosed between the brackets [ and ].
The previous grammar may be rewritten and the definition of identifiers im-
proved using these new conventions. The result is in Figure 2.7. As another
example, the context-free grammar for a simple imperative programming lan-
guage called Simp is found in Figure 2.8.
Abstract Syntax
command := SKIP
| IDENT := exp
| IF boo exp THEN command sequence
ELSE command sequence FI
| WHILE bool exp DO command sequence END
The idea is to use only as much concrete notation as is necessary to convey the
structure of the objects under description. An abstract syntax for Simp is given
in Figure 2.9. A fully abstract syntax simply gives the components of each
language construct, leaving out the representation details. In the remainder of
the text we will use the term abstract syntax whenever some syntactic details
are left out.
The lexical units (tokens) of programming languages are defined using regular
expressions. Regular expression describe how characters are grouped to form
tokens. The alphabet consists of the character set chosen for the language.
Definition 2.7 Let Σ be an alphabet. The regular expressions over Σ and the
languages (sets of strings) that they denote are defined as follows:
1. ∅ is a regular expression and denotes the empty set. This language contains
no strings.
2. is a regular expression and denotes the set {}. This language contains
one string, the empty string.
22 CHAPTER 2. SYNTAX
A D
1 2
2 2 2
3. For each a in Σ a is a regular expression and denotes the set {a}. This
language contains one string, the expression.
Identifiers and numbers are usually defined using regular expressions. If A rep-
resents any letter and D represents any digit, then identifiers and real numbers
may be defined using regular expressions as follows:
identifier = A (A + D )∗
Regular expressions are equivalent to finite state machines. A finite state ma-
chine consists of a set of states (one of which is a start state and one or more
which are accepting states), a set of transitions from one state to another each
labeled with an input symbol, and an input string. Each step of the finite state
machine consists of comparing the current input symbol with the set of transi-
tions corresponding to the current state and then consuming the input symbol
and moving to the state corresponding to the selected transition. The transi-
tions for a finite state machine which recognizes identifiers given in Figure 2.10.
The start state is 1 and the accepting state is 2. In the start state if the input
is an alphabetic character, then it is consumed and the transition to state 2
occurs. The machine remains in state 2 as long as the input consists of either
alphabetic characters or digits.
2.3. ATTRIBUTE GRAMMARS AND STATIC SEMANTICS 23
Context-free grammars are not able to completely specify the structure of pro-
gramming languages. For example, declaration of names before reference, num-
ber and type of parameters in procedures and functions, the correspondence
between formal and actual parameters, name or structural equivalence, scope
rules, and the distinction between identifiers and reserved words are all struc-
tural aspects of programming languages which cannot be specified using context-
free grammars. These context-sensitive aspects of the grammar are often called
the static semantics of the language. The term dynamic semantics is used to
refer to semantics proper, that is, the relationship between the syntax and the
computational model. Even in a simple language like Simp, context-free gram-
mars are unable to specify that variables appearing in expressions must have
an assigned value. Context-free descriptions of syntax are supplemented with
natural language descriptions of the static semantics or are extended to become
attribute grammars.
P ::= D B
D ::= V...
B ::= C ...
C ::= V := E | ...
However, this context-free syntax does not indicate this restriction. The dec-
larations define an environment in which the body of the program executes.
Attribute grammars permit the explicit description of the environment and its
interaction with the body of the program.
For regular expressions and their relationship to finite automata and context-
free grammars and their relationship to push-down automata see texts on formal
languages and automata such as[14]. The original paper on attribute grammars
was by Knuth[15]. For a more recent source and their use in compiler construc-
tion and compiler generators see [8, 23]
2.5. EXERCISES 25
2.5 Exercises
1. Construct a scanner for arithmetic expressions.
2. Lex: scanner
3. Ambiguity: if then else,
9. BNF
10. Yacc: parser
11. Context sensitivity
12. Attribute grammar: calculator
13. Construct and interpreter for BASIC.
26 CHAPTER 2. SYNTAX
Chapter 3
Semantics
There are four widely used techniques ( algebraic, axiomatic, denotational, and
operational) for the description of the semantics of programming languages.
Algebraic semantics describe the meaning of a program by defining an algebra
which defines algebraic relationships that exist among the language’s syntac-
tic elements. The relationships are described by axioms. Axiomatic semantics
method does not give the meaning of the program explicitly. Instead, proper-
27
28 CHAPTER 3. SEMANTICS
ties about language constructs are defined. The properties are expressed with
axioms and inference rules. A property about a program is deduced by using
the axioms and inference rules. Each program has a pre-condition which de-
scribes the initial conditions required by the program prior to execution and a
post-condition which describes, upon termination of the program, the desired
program property. Denotational semantics tell what is computed by giving a
mathematical object (typically a function) which is the meaning of the program.
Operational semantics tell how a computation is performed by defining how to
simulate the execution of the program. Operational semantics may describe
the syntactic transformations which mimic the execution of the program on an
abstract machine or define a translation of the program into recursive functions.
The domain is often called a sort and the domain and the semantic function
sections constitute the signature of the algebra. Functions with zero, one, and
two operands are referred to as nullary, unary, and binary operations. Often
abstract data types require values from several different sorts. Such a type is
modeled using a many-sorted algebra. The signature of such an algebra is a
set of sorts and a set of functions taking arguments and returning values of
different sorts. For example, a stack may be modeled as a many-sorted algebra
with three sorts and four operations. An algebraic definition of a stack is found
in figure 3.2.
The stack example is more abstract than the previous one because the results
of the operations are not described. This is necessary because the syntactic
structure of the natural numbers and lists are not specified. To be more specific
3.2. AXIOMATIC SEMANTICS 29
Domain:
Semantic functions:
Semantic equations:
(n + 0) = n
(m + S(n)) = S(m + n)
(n × 0) = 0
(m × S(n)) = ((m × n) + m)
where m, n ∈ Nat
Domains:
Semantic functions:
create : () → Stack
push : Nat → Stack → Stack
pop : Stack → Stack
top : Stack → Nat
empty : Stack → Bool
Semantic axioms:
pop(push(N, S)) = S
top(push(N, S)) = N
empty(push(N, S)) = f alse
empty(create()) = true
{P } c {Q}
The meaning of
{P } c {Q}
is that if c is executed in a state in which assertion P is satisfied
and c terminates, then it terminates in a state in which assertion Q
is satisfied.
The only way into the body of the while command is if the number of elements
summed is less than the number of elements in the array. When this is the case,
The sum of the first I+1 elements of the array is equal to the sum of the first
I elements plus the I+1st element and I+1 is less than or equal to n. After the
assignment in the body of the loop, the loop entry assertion holds once more.
Upon termination of the loop, the loop index is equal to n.
To show that the program is correct, we must show that the assertions satisfy
some verification scheme. To verify the assignment commands on lines 2 and 7
32 CHAPTER 3. SEMANTICS
P0
1. {0= i=1 A[i], 0 < n = |A| }
2. S,I := 0,0
PI
3. {S= i=1 A[i], I≤n}
4. while I < n do
PI
5. { S = i=1 A[i], I < n }
PI+1
6. { S+A[I+1] = i=1 A[i], I+1 ≤ n }
7. S,I := S+A[I+1],I+1
PI
8. {S= i=1 A[i], I≤n}
9. end
PI
10. { S = Pi=1 A[i], I ≤ n, I ≥ n }
n
11. { S = i=1 A[i] }
Assignment Axiom:
{P [x : E]} x := E {P }
This axiom is read as follows:
Looking at lines 1, 2 and 3 and also at lines 6, 7 and 8, we can see that this
axiom is satisfied in both cases.
To verify the while command of lines 3 through 10, we must use the following
Loop Axiom:
{I ∧ B ∧ V > 0} C {I ∧ V > V 0 ≥ 0}
{I} while B do C end {I ∧ ¬B}
The assertion above the bar is the condition that must be met before the axiom
(below the bar) can hold. In this rule, I is called the loop invariant. This axiom
is read as follows:
PI
The invariant for the loop is: S = i=1 A[i], I ≤ n. Lines 3, 5 and 8 satisfy
the condition for the application of the axiom. While lines 3 and 10 satisfy the
axiom.
To prove termination requires the existence of a loop variant. The loop variant
is an expression whose value is a natural number and whose value is decreased
on each iteration of the loop. The loop variant provides an upper bound on the
number of iterations of the loop.
The loop variant for this example is the expression n - I. That it is non-negative
is guaranteed by the loop continuation condition and its value is decreased by
one in the assignment command found on line 7.
More general loop variants may be used; loop variants may be expressions in
any well-founded set (every decreasing sequence is finite). However, there is no
loss in generality in requiring the variant expression to be an integer. Recursion
is handled much like loops in that there must be an invariant and a variant.
Rule of Consequence:
P → Q, {Q} C {R}, R → S
{P } C {S}
The justification for the composition the assignment command in line 2 and the
while command in line 4 requires the following axiom.
34 CHAPTER 3. SEMANTICS
The following rules are required to provide a logically complete deductive sys-
tem.
Selection Axiom:
{P ∧ B} C0 {Q}, {P ∧ ¬B} C1 {Q}
{P } if B then C0 else C1 fi {Q}
Conjunction Axiom:
{P } C {Q}, {P 0 } C {Q0 }
{P ∧ P 0 } C {Q ∧ Q0 }
Disjunction Axiom:
{P } C {Q}, {P 0 } C {Q0 }
{P ∨ P 0 } C {Q ∨ Q0 }
The axiomatic method is the most abstract of the semantic methods and yet,
from the programmer’s point of view, the most practical method. It is most
abstract in that it does not try to determine the meaning of a program, but
only what may be proved about the program. This makes it the most practical
since the programmer is concerned with things like, whether the program will
terminate and what kind of values will be computed.
Axiomatics semantics are the favored method by software engineers for program
verification and program derivation.
1. S,I := 0,0
2. loop: if I < n then S,I := S+A[I+1],I+1; loop
3. else skip
program and proof are developed together, the assertions themselves may pro-
vide suggestions which facilitate program construction.
Loops and recursion are two constructs that require invention on the part of
the programmer. The loop correctness principle requires the programmer to
come up with both a variant and an invariant. Recursion is a generalization
of loops so proofs of correctness for recursive programs also requires a loop
variant and a loop invariant. In the summation example, a loop variant is
readily appearent from an examination of the post-condition. Simply replace
the summation upper limit, which is a constant, with a variable. Initializing
the sum and index to zero establishes the invariant. Once the invariant is
established, either the index is equal to the upper limit in which case there
sum has been computed or the next value must be added to the sum and the
index incremented reestablishing the loop invariant. The position of the loop
invariants define a loop body and the second occurrence suggests a recursive
call. A recursive version of the summation program is given in figure 3.4.
The advantage of using recursion is that the loop variant and invariant may be
developed separately. First develop the invariant then the variant.
Abstract Syntax:
Semantic Algebra:
Valuation Function:
D ∈ Nat → Nat
the syntactic and semantic domains. The syntactic expressions are mapped into
an algebra of the natural numbers by the valuation function. The denotational
definition almost seem to be unnecessary. Since the syntax so closely resem-
bles that of the semantic algebra. Programming languages are not as close to
their computational model. Figure 3.6 is denotational definition of the small
imperative programming language Simp encountered in the previous chapter.
Abstract Syntax:
C ∈ Command
E ∈ Expression
O ∈ Operator
N ∈ Numeral
V ∈ Variable
Semantic Algebra:
Domains:
τ ∈ T = {true, false} boolean values
ζ ∈ Z = {...-1,0,1,...} integers
σ ∈ S = Variable → Numeral state
Valuation Functions:
C ∈ C → (S → S)
E ∈ E → E → (N ∪ T)
C[[skip]]σ = σ
C[[V := E]]σ = σ[V : E[[E]]σ]
C[[C1 ; C2 ]]σ = C[[C2 ]]C[[C1 ]]σ
C[[C1 ]]σ if E[[E]]σ = true
C[[if E then C1 else C2 end]]σ =
C[[C2 ]]σ if E[[E]]σ = false
n
C[[while E do C end]]σ= limn→∞ C[[(if E then C else skip end) ]]σ
E[[V ]]σ = σ(V )
E[[N ]] = N
E[[E1 + E2 ]] = E[[E]]σ + E[[E]]σ
...
E[[E1 = E2 ]]σ = E[[E]]σ = E[[E]]σ
...
Abstract Syntax:
N ::= 0 | S(N) | (N + N) | (N × N)
Interpreter:
I:N→N
I[[(n + 0)]] ⇒ n
I[[(m + S(n))]] ⇒ S(I[[(m + n)]])
I[[(n × 0)]] ⇒ 0
I[[(m × S(n))]] ⇒ I[[((m × n) + m)]]
where m, n ∈ Nat
mand is given a recursive definition but may be defined using the interpreter
instead.
Algebraic semantics
Gries [10] and Hehner [11] are excellent introductions to axiomatic semantics as
applied to program construction. For a description of denotational semantics
see Schmidt [26] or Scott [28]
Operational semantics??
40 CHAPTER 3. SEMANTICS
Interpreter:
I :C×Σ→Σ
ν ∈ E ×Σ → T ∪ Z
Semantic Equations:
I(skip,σ) = σ
I(V := E,σ) = σ[V:ν(E,σ)]
I(C1 ;C2 ,σ) = E(C2 ,E(C1 ,σ))
I(C1 ,σ) if ν(E,σ) = true
I(if E then C1 else C2 end,σ) =
I(C2 ,σ) if ν(E,σ) = false
while E do C end = if E then C;while E do C end else skip
ν(V,σ) = σ(V)
ν(N,σ) = N
ν(E1 +E2 ,σ) = ν(E1 ,σ) + ν(E2 ,σ)
...
true if ν(E,σ) = ν(E,σ)
ν(E1 =E2 ,σ) = false if ν(E,σ) 6= ν(E,σ)
otherwise
...
Abstraction and
Generalization I
Abstraction is an emphasis on the idea, qualities and properties rather than the
particulars (a suppression of detail).
The importance of abstraction is derived from its ability to hide irrelevant de-
tails and from the use of names to reference objects. Programming languages
provide abstraction through procedures, functions, and modules which permit
41
42 CHAPTER 4. ABSTRACTION AND GENERALIZATION I
Substitution E[x : a]
Generalization λx.E
Specialization (λx.E a) = E[x : a]
the programmer to distinguish between what a program does and how it is im-
plemented. The primary concern of the user of a program is with what it does.
This is in contrast with the writer of the program whose primary concern is
with how it is implemented. Abstraction is essential in the construction of pro-
grams. It places the emphasis on what an object is or does rather than how
it is represented or how it works. Thus, it is the primary means of managing
complexity in large programs.
Abstraction and generalization are often used together. Abstracts are general-
ized through parameterization to provide greater utility. In parameterization,
one or more parts of an entity are replaced with a name which is new to the
entity. The name is used as a parameter. When the parameterized abstract is
invoked, it is invoked with a binding of the parameter to an argument. Figure 4.1
summarizes the notation which will be used for abstraction and generalization.
4.1 Abstraction
Principle of Abstraction: An abstract is a named entity which may be in-
voked by mentioning the name.
Giving an object a name gives permission to substitute the name for the thing
named (or vice versa) without changing the meaning. We use the notation
name : abstract
Binding
4.2 Generalization
Principle of Generalization: A generic is an entity which may be elaborated
upon invocation.
Generalization is often combined with abstraction and takes the following form:
p(x) : B
where p is the name, x is the parameter, and B is the abstract. The invocation
of the abstract takes the form:
(p a) or p(a)
where p is the name and a is called the argument whose value (called the ar-
gument) is substituted for the parameter. Upon invocation of the abstract, the
argument is bound to the parameter.
4.2. GENERALIZATION 45
pi : 3.14
c : 2*pi*r
begin
r := 5
write c
r := 20
write c
end
The value of r depends on the context in which the function is defined. The
variable r is a global name and is said to be free. In the first write command,
the circumference is computed for a circle of radius 5 while in the second write
command the circumference is computed for a circle of radius 20. The write
commands cannot be understood with reference to both the definition of c and
to the environment (pi is viewed as a constant). Therefore, this program is not
“fully abstract”. In contrast, the following program is fully abstract:
pi : 3.14
c(r) : 2*pi*r
begin
FirstRadius := 5
write c(FirstRadius)
SecondRadius := 20
write c(SecondRadius)
end
4.3 Substitution
Terminology: The notation for substitution was chosen to emphasize the re-
lationship between abstraction and substitution. Other texts use the notation
E[a/x] for substitution. That notation is motivated by the cancelation that
occurs when a number is multiplied by its inverse ( x(a/x) = a). 2
4.5 Exercises
1. Extend the compiler to handle constant, type, variable, function and pro-
cedure definitions and references to the same.
2. What is the effect of providing lazy evaluation in an imperative program-
ming language?
A value is any thing that may be evaluated, stored, incorporated in a data struc-
ture, passed as an argument or returned as a result.
What is a type?
Idealist: No. A type is a conceptual entity whose values are accessible only
through the interpretive filter of type.
Type checker: Types are more practical than that, they are constraints on
expressions to ensure compatibility between operators and their operand(s).
Type Inference System: Yes and more, since a type system is a set of rules
for associating with every expression a unique and most general type that
reflects the set of all meaningful contexts in which the expression may
occur.
Program verifier: Lets keep it simple, types are the behavioral invariants that
instances of the type must satisfy.
49
50 CHAPTER 5. DOMAINS AND TYPES
Software engineer: What is important to me is that types are a tool for man-
aging software development and evolution.
Compiler: All this talk confuses me, types specify the storage requirements for
variables of the type.
51
Keywords and phrases: value, domain, type, type constructor, Cartesian prod-
uct, disjoint union, map, power set, recursive type, binding, strong and weak
typing, static and dynamic type checking, type inference, type equivalence, name
and structural equivalence, abstract types, generic types.
In mathematical terminology, the sets from which the arguments and results of
a function are taken are known as the function’s “domain” and “codomain”,
respectively. Consequently, the term domain will denote any set of values that
can be passed as arguments or returned as results. Associated with every domain
are certain “essential” operations. For example, the domain of natural numbers
is equipped with an the “constant” operation which produces the number zero
and the operation that constructs the successor of any number. Additional
operations (such as addition and multiplication) on the natural numbers may
be defined using these basic operations.
The terms domain, type, and data type may be used interchangeably.
The terms compound, composite and structured when applied to values, data,
domains, types are used interchangeably. 2
The values are represented as a particular pattern of bits in the storage of the
computer.
Several languages permit the user to define additional primitive types. These
primitive types are called enumeration types.
There are many compound domains that are useful in computer science: arrays,
tuples, records, variants, unions, sets, lists, trees, files, relations, definitions,
mappings, etc, are all examples of compound domains. Each of these domains
may be constructed from simpler domains by one or more applications of domain
constructors.
• Product Domains
5.2. COMPOUND DOMAINS 53
• Sum Domains
• Function Domains
• Power Domains
• Recursive Domains
Product Domain
The domains constructed by the product domain builder are called tuples in
ML, records in Cobol, Pascal and Ada, and structures in C and C++. Product
domains form the basis for relational databases and logic programming.
In the binary case, the product domain builder × builds the domain A × B from
domains A and B. The domain builder includes the assembly operation, ordered
pair builder, and a set of disassembly operations called projection functions. The
assembly operation, ordered pair builder, is defined as follows:
A × B = { (a, b) | a ∈ A, b ∈ B }
The disassembly operations fst and snd are projection functions which extract
elements from tuples. For example, fst extracts the first component and snd
extracts the second element.
fst(a, b) = a
snd(a, b) = b
The product domain is easily generalized (see Figure 5.1 to permit the product
of an arbitrary number of domains.
Both relational data bases and logic programming paradigm (Prolog) are based
on programming with tuples.
54 CHAPTER 5. DOMAINS AND TYPES
Sum Domain
Domains constructed by the sum domain builder are called variant records in
Pascal and Ada, unions in Algol-68, constructions in ML and algebraic types in
Miranda.
In the binary case, the sum domain builder + builds the domain A + B from
domains A and B. The domain builder includes a pair of assembly operations
and the disassembly operation. The two assembly operations of the sum builder
are defined as follows:
A + B = {(A, a) | a ∈ A} ∪ {(B, b) | b ∈ B }
where the A and B are called tags and are used to distinguish between
the elements contributed by A and the elements contributed by B.
The disassembly operation returns the element iff the tag matches the request.
A(A, a) = a
The sum domain differs from ordinary set union in that the elements of the
union are labeled with the parent set. Thus even when two sets contain the
same element, the sum domain builder tags them differently.
The sum domain generalizes (see Figure 5.2) to sums of an arbitrary number
of domains.
Terminology: The sum domain is also called the disjoint union or co-product
domains. 2
5.2. COMPOUND DOMAINS 55
Function Domain
The domains constructed by the function domain builder are called functions in
Haskell, procedures in Modula-3, and procs in SR. Although their syntax differs
from functions, arrays are also examples of domains constructed by the function
domain builder.
The function domain builder creates the domain A → B from the domains A
and B. The domain A → B consists of all the functions from A to B. A is called
the domain and B is called the co-domain.
Mappings (or functions) from one set to another are an extremely important
compositional method. The map m from a element x of S (called the domain)
56 CHAPTER 5. DOMAINS AND TYPES
Arrays are mappings from an index set to an array element type. An array is a
finite mapping. Apart from arrays, mappings occur as operations and function
abstractions. Array values are implemented by allocating a contiguous block
of storage where the size of the block is based on the product of the size of an
element of the array and the number of elements in the array.
The operations provided for the primitive types are maps. For example, the
addition operation is a mapping from the Cartesian product of numbers to
numbers.
+ : number × number → number
Power Domain
The set of all subsets of a set is the power set and is defined:
PS = { s | s ⊆ S }
Subtypes and subranges are examples of the power set constructor.
Functions are subsets of product domains. For example, the square function
can be represented as a subset of the product domain N at × N at.
sqr = {(0, 0), (1, 1), (2, 4), (3, 9), ...}
5.2. COMPOUND DOMAINS 57
Set values may be implemented by using the underlying hardware for bit-strings.
This makes set operations efficient but constrains the size of sets to the num-
ber of bits(typically) in a word of storage. Alternatively, set values may be
implemented using software, in which case, hash-coding or lists may be used.
More than one set may satisfy a recursive definition. However, it may be shown
that a recursive definition always has a least solution. The least solution is a
subset of every other solution.
58 CHAPTER 5. DOMAINS AND TYPES
D0 = null
Di+1 = e[D:Di ] for i = 0...
D = limi→∞ Di
N ::= 0 | S(N)
N0 = N ull
Ni+1 = 0 | S(Ni ) for i = 0...
N0 = N ull
N1 =0
N2 = 0 | S(0)
N3 = 0 | S(0) | S(S(0))
N4 = 0 | S(0)|S(S(0)) | S(S(S(0)))
...
The proof of this last equation is beyond the scope of this text. This construction
suggests that recursive definitions can be understood in terms of a family of non-
recursive definitions and in format common to each member of the family.
Ancestors
For logical predicates, Null can be replaced with false. A recursive definition
of the ancestor relation is:
parent(A, D) or
ancestor(A, D), if
parent(A, I) & ancestor(I, D)
The ancestor relation is approximated by a sequence of relations:
ancestor0 (A, D) = false
And the relation ancestori is defined as
parent(A, D) or
ancestori+1 (A, D), if
parent(A, I) & ancestori (I, D)
Writing the relations as sets of order pairs helps us to understand the limit
construction. An example will help. Suppose we have the following:
then we have:
ancestor0 = {}
ancestor1 = {(John, Mary), (Mary, James), (James, Alice)}
ancestor2 = ancestor1 ∪ {(John, James), (Mary, Alice)}
ancestor3 = ancestor2 ∪ {(John,Alice)}
Again note that each predicate in the sequence includes the previously defined
predicate and the sequence suggests that
Linear Search
Since recursively defined domains like lists, stacks and trees are unbounded (in
general may be infinite objects) they are implemented using pointers. In Pascal,
Ada and C such domains are defined in terms of pointers while Prolog and
functional languages like ML and Miranda allow recursive types to be defined
directly.
An abstract type is a type which is defined by its operations rather than its values.
The data types provided in programming languages are abstract types. For
example, the representation of the integer type is hidden from the programmer.
5.3. ABSTRACT TYPES 61
An Abstract data type consists of a type name and operations for creating and
manipulating objects of the type. A key idea is that of the separation of the
implementation from the type definition. The actual format of the data is hidden
(information hiding) from the user and the user gains access to the data only
through the type operations.
In order to be fully abstract, the user of the abstract type must not be per-
mitted access to the representation of values of the type. This is the case with
the primitive types. For example, integers might be represented in two’s com-
plement binary numbers but there is no way of finding out the representation
without going outside the language. Thus, a key concept of abstract types is
the hiding of the representation of the values of the type. This means that the
representation information must be local to the type definition.
Since the representation of the values of the type is hidden, abstract types
must be provided with constructor and destructor operations. A constructor
operation composes a value of the type from values from some other type or types
while a destructor operation extracts a constituent value from an abstract type.
For example, an abstract type for rational numbers might represent rational
numbers as pairs of integers. This means that the definition of the abstract
type would include an operation which given a pair of integers returns a rational
number (whose representation as an ordered pair is hidden) which corresponds
to the the quotient of the two numbers. The rational additive and multiplicative
identities corresponding to zero and one would be provided also.
Terminology: The terms abstract data type and ADT are also used to denote
what we call an abstract type. 2
62 CHAPTER 5. DOMAINS AND TYPES
TYPE Complex;
TYPE
Complex = POINTER TO ComplexData
ComplexData = RECORD
RealPart, ImPart : REAL;
END;
BEGIN
new( result );
result↑.RealPart := firstNum;
result↑.ImPart := secondNum
return result
END NewComplex;
BEGIN
new( result );
result↑.RealPart := firstNum↑.RealPart + secondNum↑.RealPart;
result↑.ImPart := firstNum↑.ImPart + secondNum↑.ImPart
return result
END AddComplex;
...
BEGIN
...
END ComplexNumbers.
TYPE stack(ElementType);
Given an abstract type stack, the stack items would be restricted to be a specific
type. This means that an abstract type definition is required for stacks which
differ only in the element type contained in the stack. Since the code required
by the stack operations is virtually identical, a programmer should be able to
write the code just once and share the code among the different types of stacks.
Generic types or generics are a mechanism to provide for sharing the code. The
sharing provided by generics is through permitting the parameterization of type
definitions. Figure 5.8 contains a Modula-2 definition module for a generic stack.
The definition differs from that of an abstract type in that the type name is
parameterized with the element type. At compile time, code appropriate to the
parameter is generated.
The problem of writing generic sorting routines points out some difficulties
with traditional programming languages. A sorting procedure must be able to
detect the boundaries between the items it is sorting and it must be able to
compare the items to determine the proper ordering among the items. The
first problem is solved by parameterizing the sort routine with the type of the
items and the second is solved by either parameterizing the sort routine with
a compare function or by overloading the relational operators to permit more
general comparisons.
Definition 5.1 A type system is a set of rules for associating a type with ex-
pression in the language. A type system rejects an expression if it does not
associate a type with the expression.
If the error detection is to be delayed until execution time, then dynamic type
checking is required. The programming languages Scheme and Prolog do not
require the programmer to provide any information concerning an object’s type
and type checking is necessarily delayed until run time.
Type Equivalence
Two unnamed types (sets of objects) are the same if they contain the same
elements. The same cannot be said of named types for if they were, then there
would be no need for the disjoint union type. When types are named, there are
two major approaches to determining whether two types are equal.
Name Equivalence
In name equivalence two types are the same iff they have the same name. Name
equivalence was chosen for Modula-2, Ada, C (for records), and Miranda and
requires type definitions to be global. The predecessor of Modula-2, Pascal
violates name equivalence since file type names are not required to be shared
by different programs accessing the same file.
Structural Equivalence
In structural equivalence, the names of the types are ignored and the elements
of the types are compared for equality. Formally,
Definition 5.2 Two types T, T0 are structurally equivalent iff T, T0 have the
same set of values.
The following three rules may be used to determine if two types are structurally
equivalent.
Structural equivalence was chosen by Algol-68 and C (except for records) be-
cause it is easy to implement and type definitions are not required to be global.
For example, if the boolean constants are represented by 0 and 1 and the lan-
guage permits a boolean to occur in arithmetic expressions, then the language
is not enforcing the boolean type abstraction.
Most languages are strongly typed with respect to the primitive types supported
by the language.
Strong typing helps to insure the security and portability of the code and it
often requires the programmer to explicitly define the types of each object in a
program.
SORT PROGRAMS
SETS VS LISTS
Type Inference
Pascal constant declarations are an example of type inference, the type of the
name is inferred from the type of the constant. In Pascal’s for loop the type of
the loop index can be inferred from the types of the loop limits and thus the
loop index should be a variable local to the loop. The programming language
Miranda provides a powerful type inference system so that a programmer need
not declare any types. However, Miranda is strongly typed.
If a language requires that the type of all variables be known compile time,
then the a language is said to be statically typed. Pascal, Ada, and Haskell are
5.6. OVERLOADING AND POLYMORPHISM 67
If a language does not require that the type of a variable be known at compile
time, then a language is said to be dynamically typed. Lisp and Smalltalk are
examples of dynamically typed languages.
Dynamic type checking implies that the types are checked at execution time
and that every value is tagged to identify its type in order to make the type
checking possible. The penalty for dynamic type checking is additional space
and time overheads.
Dynamic typing is often justified on the assumption that its flexibility permits
the rapid prototyping of software.
Overloading
When the same operation symbol is used for the plus operation for rational
numbers and for set union, the symbol as in Pascal it is overloaded. Most
programming languages provide for the overloading of the arithmetic operators.
68 CHAPTER 5. DOMAINS AND TYPES
Many languages provide type transfer functions so that the programmer can
control where and when the type coercion is performed. Truncate and round
are examples of type transfer functions.
Polymorphism
Sometimes there are several domains which share a common operation. For
example, the natural numbers, the integers, the rationals, and the reals all
share the operation of addition. So, most programming languages use the same
addition operator to denote addition in all of these domains. Pascal extends the
use of the addition operator to represent set union. The multiple use of a name
in different domains is called overloading. Ada permits user defined overloading
of built in operators.
Prolog permits the programmer to use the same functor name for predicates
of different arity thus permitting the overloading of functor names. This is an
example of data generalization or polymorphism.
5.7. TYPE COMPLETENESS 69
While the parameterization of an object gives the ability to deal with more than
one particular object, polymorphism is the ability of an operation to deal with
objects of more than a single type.
5.8 Exercises
1. Define algebraic semantics for the following data types.
(a) Boolean
ADTBoolean
Operations
and(boolean,boolean) → boolean
or(boolean,boolean) → boolean
not(boolean) → boolean
Semantic Equations
and(true,true) = true
or(true,true) = true
not(true) = false
not(false) = true
Restrictions
(b) Integer
(c) Real
(d) Character
(e) String
2. Name or Structure equivalence (type checking)
3. Algebraic Semantics: stack, tree, queue, grade book etc
4. Abstraction
5. Generalization
70 CHAPTER 5. DOMAINS AND TYPES
Environment
Keywords and phrases: block, garbage collection, static and dynamic links,
display, static and dynamic binding, activation record, environment, Static and
Dynamic Scope, aliasing, variables, value, result, value-result, reference, name,
unification, eager evaluation, lazy evaluation, strict, non-strict, Church-Rosser,
overloading, polymorphism, monomorphism, coercion, transfer functions.
Scope has to do with the range of visibility of names. For example, a na-
tional boundary may encapsulate a natural language. However, some words
used within the boundary are not native words. They are words borrowed from
some other language and are defined in that foreign language. So it is in a
program. A definition introduces a name and a boundary ( the object ). The
object may contain names for which there is no local definition (assuming defi-
nitions may be nested). These names are said to be free. The meaning assigned
to these names is to be found outside of the definition. The rules followed in
determining the meaning of these free names are called scope rules.
Scope is the portion of the program text over which a definition is effective.
71
72 CHAPTER 6. ENVIRONMENT
Binding Sequence
Collateral
Collateral bindings are not vary common but occur in Scheme and ML.
Sequential
In Pascal, constant, variable, and procedure and function bindings are sequen-
tial. To provide for mutually recursive definitions of functions and procedures,
Pascal provides for the separation of the signature of a function or procedure
from the body by the means of forward declarations so that so that mutually
recursive definitions may be constructed.
Recursive
A recursive binding is one that uses the very bindings that it produces itself.
In Pascal type definitions are recursive to permit the construction of recursive
types. However, there is a constraint to insure that recursive types are restricted
to pointer types.
A block is a construct that delimits the scope of any declarations that it may
contain. It provides a local environment i.e., a opportunity for local definitions.
The block structure (the textual relationship between blocks) of a programming
6.1. BLOCK STRUCTURE 73
language has a great deal of influence over program structure and modularity.
There are three basic block structures–monolithic, flat and nested.
A program has a monolithic block structure if it consists of just one block. This
structure is typical of BASIC and early versions of COBOL. The monolithic
structure is suitable only for small programs. The scope of every definition is
the entire program. Typically all definitions are grouped in one place even if
they are used in different parts of the program.
A program has nested block structure if blocks may be nested inside other blocks.
This is typical of the block structure of the Algol-like languages. A block can
be located close to the point of use.
A local variable is one that is declared within a block for use only within that
block.
The three basic block structures are sufficient for what is called programming in
the small. These are programs which are comprehensible in their entirety by an
individual programmer. They are not general enough for very large programs.
Programs which are written by many individual and which must consist of
module which can be developed and tested independently of other modules.
Such programming is called programming in the large. The subject of modules
will be examined in a later chapter.
Activation Records
Each block
Scope Rules
A dynamic scope rule defines the dynamic scope of each association in terms of
the dynamic course of program execution. Lisp.
6.2 Declarations
6.3 Constants
literals
Declarations of variables of the primitive types have the following form in the
imperative languages.
Haskell provides the built in functions fst and snd to extract the first and
second elements from binary tuples.
record
I1 : T1 ;
...
In : Tn ;
end
The Ii s are names for the component of the tuple. The individual components
of the record are accessed by the use of qualified names. for example, if MyRec is
a elment of the above type, then the first component is referenced by MyRec.I1
and the last component is referenced by MyRec.In .
C and C++ calls a product domain a structure and uses the following type
declaration:
struc name {
T1 I1 ;
...
Tn : In ;
};
Prolog does not require type declaration and elements of a product domain may
be represented in a number of ways, one way is by a term of the form:
name(I1 ,...In )
The Ii s are the entries in the tuple. The entries are accessed by pattern match-
ing. Miranda does not require type declaration and the elements of a product
domain are represented by tuples.
(I1 ,...In )
The initialization of the record should follow the sequence of assigning a value
to the tag and then to the appropriate subfields.
FourSidedObject.WhatShape := Rectangle;
FourSidedObject.Length := 4.3;
FourSidedObject.Width := 7.5;
Disjoint union values are implemented by allocating storage based on the largest
possible value and additional storage for the tag.
Modula-2
Prolog and Miranda do not provide for an array type and while Scheme does,
it is not a part of the purely functional part of Scheme.
6.4. USER DEFINED TYPES 77
In Pascal the notation [i..j] indicates the subset of an ordinal type from element
i to element j inclusive. In addition to subranges, Miranda provides infinite
lists [i..] and finite and infinite arithmetic series [a,b..c], [a,b..] (the interval is
(b-a)). Miranda also provides list comprehensions which are used to construct
lists (sets). A list comprehension has the form [exp | qualifier]
Prolog
[]
The Miranda syntax for lists is similar to that of Prolog however, elements of
lists must be all of the same type.
[*]
[ T ]
tree ::= Niltree | Node T tree tree
Referencing/Dereferencing
Recursive values are implemented using pointers. The run-time support system
for the functional and logic programming languages, provides for automatic
allocation and recovery of storage (garbage collection). The alternative is for
the language to provide access to the run-time system so that the programmer
can explicitly allocate and recover the linked structures.
6.5 Variables
state:store
Most programming languages provide different types for persistent and transient
variables. Typically files are the only type of persistent variable permitted in a
programming language. Files are not usually permitted to be transient variables.
Most programming languages provide different types for persistent and transient
The type completeness principle suggests that all the types of the programming
language should be allowed both for transient variables and persistent variables.
A language applying this principle would be simplified by having no special
6.8. EXERCISES 79
6.8 Exercises
1. Extend the compiler to handle constant, type, variable, function and pro-
cedure definitions and references to the same.
Functional Programming
Keywords and phrases: Lambda calculus, free and bound variables, scope, en-
vironment, functional programming, combinatorial logic, recursive functions,
functional, curried function.
Functional programming languages are the result of both abstracting and gen-
eralizing the data type of maps. Recall, the mapping m from each element x of
S (called the domain) to the corresponding element m(x) of T (called the range)
is written as:
m : S→T
For example, the squaring function is a function of type:
sqr : N um → N um
and may be defined as:
sqr
x 7−→ x ∗ x
A linear function f of type
f : N um → N um
may be defined as:
f
x 7−→ 3 ∗ x + 4
81
82 CHAPTER 7. FUNCTIONAL PROGRAMMING
The function:
g
x 7−→ 3 ∗ x2 + 4
may be written as the composition of the functions f and sqr as:
f ◦ sqr
where
f ◦ sqr(x) = f (sqr(x)) = f (x ∗ x) = 3 ∗ x2 + 4
The compositional operator is an example of a functional form. Functional
programming is based on the mathematical concept of a function and functional
programming languages include the following:
LISP, FP, Scheme, ML, Miranda and Haskell are just some of the languages to
implement this elegant computational paradigm.
λx.x2 + 3x − 5
is used to denote the polynomial function whose values are given by the polyno-
mial expression. Thus, the lambda abstraction is can be read as “the function
of x whose value is x2 + 3x − 5.”
p = λx.x2 + 3x − 5
(λx.x2 + 3x − 5 1)
The value is obtained by applying the lambda abstraction to the 1. The appli-
cation of a lambda abstraction to a value entails the substitution of 1 for x to
obtain 12 + 3 · 1 − 5 which evaluates to −1.
We say that the variable x is bound in the body B of the lambda expression
λx.B. A variable which is not bound in a lambda expression is said to be free.
84 CHAPTER 7. FUNCTIONAL PROGRAMMING
Abstract Syntax:
L ∈ Lambda Expressions
x ∈ Variables
The variable x is free in the lambda expression λy.x + y). The scope of the
variable introduced (or bound) by lambda is the entire body of the lambda
abstraction.
The pure lambda calculus has just three constructs: variables, function applica-
tion, and function creation. Figure 7.1 gives the syntax of the lambda calculus.
The notation λx.M is used to denote a function with parameter x and body
M . For example, λx.(+ x x) denotes the function that maps 3 to 3 + 3. Notice
that functions are written in prefix form so in this instance the application of
the function to 3 would be written: λx.(+ x x) 3.
The pure lambda calculus does not have any built-in functions or constants.
Therefore, it is appropriate to speak of the lambda calculi as a family of lan-
guages for computation with functions. Different languages are obtained for
different choices of constants.
We will extend the lambda calculus with common mathematical operations and
constants so that λx.((+ 3) x) defines a function that maps x to x + 3. We
will drop some of the parentheses to improve the readability of the lambda
expressions.
Operational Semantics
(λx.((+ 3) x)) 4
((+ 3) 4)
7
(λx.B M ) ↔ B[x : M ]
One problem which arises with β-reduction is the following. Suppose we apply
β-reduction as follows.
(λx.B M ) ↔ B[x : M ]
where y is a free variable in M but y occurs bound in B. Then upon the
substitution of M for x, y becomes bound. To prevent this from occurring, we
need to do the following.
λx.B ↔ λy.B[x : y]
x[x : M ] = M
c[x : M ] = c where c is a variable or constant other than x.
(A B)[x : M ] = (A[x : M ] B[x : M ])
(λx.B)[x : M ] = (λx.B)
(λy.B)[x : M ] = λz.(B[y : z][x : M ]) where z is a new variable name
which does not occur free in B or M .
Figure 7.2 defines the operational semantics of the lambda calculus in terms of
β-reduction.
Reduction Order
Given a lambda expression, the previous section provides the tools required to
evaluate the expression but, the previous section did not tell us what order we
should use in reducing the lambda expressions.
Interpreter:
I∈L→L
I[[c]] = c
I[[x]] = x
I[[(λx.B M )]] = I[[B[x : M ]]]
I[[(L1 L2 )]] = σ(φ)
where I[[L1 ]] = σ, I[[L2 ]] = φ
where
c is a constant
x is a variable
B, L1 , L2 , M are expressions
Normal order reduction specifies that the leftmost outermost redex should be
reduced first.
Denotational Semantics
Eval[[+ 3 4]] = 7
is required, in the case of variables and function names, the function Eval
requires a second parameter containing the environment ρ which contains the
associations between variables and their values. Some programs go into infinite
loops, some abort with a runtime error. To handle these situations we introduce
the symbol ⊥ pronounced ‘bottom’.
Eval[[c]] = c
Eval[[x]] ρ = ρx
Eval[[L1 L2 ]] ρ = (Eval[[L1 ]] ρ) (Eval[[L2 ]] ρ)
Eval[[λx.B]] ρ a = Eval[[λx.B]] ρ[x : a]
Eval[[E]] = ⊥
where c is a constant or built-in function, x is a variable, B, L1 , L2 are expres-
sions and E is an expression which does not have a normal form.
Lambda Expressions
We can treat the recursive call as a free variable and replace the previous defi-
nition with the following.
FAC = (λfac.(λn.(if (= n 0) 1 (∗ n (fac (− n 1))))) FAC)
Let
H = λfac.(λn.(if (= n 0) 1 (∗ n (fac (− n 1)))))
Note that H is not recursively defined. Now we can redefine FAC as
FAC = (H FAC)
7.2. RECURSIVE FUNCTIONS 89
Semantic Domains:
EV = Expressible values
Semantic Function:
E ∈L→L
B ∈ Constants → EV
Semantic Equations:
E[[c]] = B[[c]]
E[[x]] = x
E[[(λx.B M )]] = E[[B[x : M ]]]
E[[(L1 L2 )]] = σ(φ)
where E[[L1 ]] = σ, E[[L2 ]] = φ
E[[E]] = ⊥
where
c is a constant
B, L1 , L2 , M are expressions
E is an expression which does not have a normal form
and
x[x : M ] = M
vc[x : M ] = vc where vc is a variable or constant other than x.
(A B)[x : M ] = (A[x : M ] B[x : M ])
(λx.B)[x : M ] = (λx.B)
(λy.B)[x : M ] = λz.(B[y : z][x : M ]) where z is a new variable name
which does not occur free in B or M .
This equation is like a mathematical equation. It states that when the function
H is applied to FAC, the result is FAC. We say that FAC is a fixed point or
fixpoint of H. In general functions may have more than one fixed point. In this
case the desired fixed point is the mathematical function factorial. In general,
the ‘right’ fixed point turns out to be the unique least fixed point.
Lexical scope rules vars refer to nearest enclosing environment, parameters are
passed, after renaming, by textual substitution
7.4. FUNCTIONAL FORMS 91
Lambda Expressions
L ∈ Lambda Expressions
x ∈ Variables
...
The let extension is for non-recursive definitions and the letrec extension is for
recursive definitions Here is a simple let example.
let x = 3 in (∗ x x)
Lets may be used where ever a lambda expression is permitted. For example,
λy.let x = 3 in (∗ y x)
is equivalent to
λy.(∗ y 3)
Let expressions may be nested. Actually lets do not extend the lambda cal-
culus they are just an abstraction mechanism as this equivalence shows.
(let v = B in E) ≡ ((λv.E) B)
Letrecs do not extend the lambda calculus they are also just an abstraction
mechanism as this equivalence shows.
double (x) = x * x
quad = double ◦ double
twice (f) = f ◦ f
odd = not ◦ even
Functionals are often used to construct reusable code and to obtain much of the
power of imperative programming within the functional paradigm.
parallel
currying
call by need
Scheme evaluates its parameters before passing (eliminates need for renaming)
a space and time efficiency consideration.
Concurrent evaluation
For example,
f 0 = 1
f n+1 = (n+1)*(f n)
fp 0 fn = fn
fp n+1 in = fp n (n+1)*in
f n = fp n 1
f 0 = fp 0 1 by definition of f and fp
assume f n = fp n 1
94 CHAPTER 7. FUNCTIONAL PROGRAMMING
fold
unfold
f 0 = 1
f (n+1) = (n+1)*f(n)
insert (item Empty Tree) = BST item Empty Tree Empty Tree
insert (item BST x LST RST) = BST x insert (item LST) RST if item < x
BST x LST insert( item RST ) if item > x
Curry, Feys, and Craig define a number of combinators among them the follow-
ing:
S = λf.(λg.(λx.f x(gx)))
K = λx.λy.x
I = λx.x
Y = λf.λx.(f (x x))λx.(f (x x))
These definitions lead to transformation rules for sequences of combinators. The
reduction rules for the SKI calculus are given in Figure 7.4. The reduction
rules require that reductions be performed left to right. If no S, K, I, or Y
reduction applies, then brackets are removed and reductions continue.
7.10. COMBINATORIAL LOGIC 95
• S f g x → f x (g x)
• Kcx→c
• Ix→x
• Y e → e (Y e)
• (A B) → A B
• (A B C)→ A B C
C[[CV ]] → CV
C[[(E1 E2 )]] → (C[[E1 ]] C[[E2 ]])
C[[λx.E]] → A[[(x, C[[E]])]]
A[[(x, x)]] → I
A[[(x, c)]] → (Kc)
A[[(x, (E1 E2 ))]] → ((S A[[(x, E1 )]]) A[[(x, E2 )]])
Figure 7.5: Translation rules for the lambda calculus to SKI code
The SKI calculus is computationally complete; that is, these three operations
are sufficient to implement any operation. This is demonstrated by the rules in
Figure 7.5 which translate lambda expressions to formulas in the SKI calculus.
• S (K e) (K f) → K (e f)
• S (K e) I → e
• S (K e) f → (B e) f
• S e (K f) → (C e) f
The optimizations must be applied in the order given.
Optimizations
Notice that the size of the SKI code grows quadratically in the number of bound
variables. Figure 7.6 contains a number of optimizations for SKI code. Among
the optimizations are two additional combinators, the B and C combinators.
7.11 Scheme
Scheme has two kinds of objects, atoms and lists. Atoms are represented by
strings of non-blank characters. A list is represented by a sequence of atoms
or lists separated by blanks and enclosed in parentheses. Functions in Scheme
are also represented by lists. This facilitates the creation of functions which
create other functions. A function can be created by another function and then
the function applied to a list of arguments. This is an important feature of
languages for AI applications.
Syntax
Scheme Syntax
E ∈ Expressions
A ∈ Atoms ( variables and constants )
...
E ::= A | (E...) | (lambda (A...) E) | ...
Expressions are atoms which are variables or constants, lists of arbitrary length
(which are also function applications), lambda abstractions of one or more pa-
rameters, and other built-in functions. Scheme permits lambda abstractions of
more than one parameter.
Definitions
The list is the basic data structure. Among the built in functions for list ma-
nipulation provided in Scheme are cons for attaching an element to the head
of a list, car for extracting the first element of a list, and cdr which returns a
list minus its first element. Figure 7.7 contains an example of stack operations
writtem in Scheme. The figure illustrates definitions, the conditional expres-
sion, the list predicate null? for testing whether a list is empty, and the list
manipulation functions cons, car, and cdr.
98 CHAPTER 7. FUNCTIONAL PROGRAMMING
( define push
( lambda ( element stack ) ( cons element stack ) ))
(define pop
( lambda ( element stack ) ( cdr stack )))
(define top
( lambda ( stack ) ( car stack )))
Local Definitions
Scheme Syntax
...
B ∈ Bindings
...
The let values are computed and bindings are done in parallel, the let* values
and bindings are computed sequentially and the letrec bindings are in effect
while values are being computed to permit mutually recursive definitions.
7.12 Haskell
Figure 7.8
module Qs where
7.12. HASKELL 99
primes :: [Int]
primes = map head (iterate sieve [2 ..])
pascal :: [[Int]]
pascal = [1] : [[x+y | (x,y) <- zip ([0]++r) (r++[0])] | r <- pascal]
half [] = []
half [x] = [x]
half (x:y:z) = x:r where r = half z
sort [] = []
sort [x] = [x]
sort l = merge (sort odds) (sort evens) where
odds = half l
evens = half (tail l)
this
7.14 Exercises
1. Simplify the following expressions to a final (normal) form, if one exists.
If one does not exist, explain why.
2. In addition to the β-rule, the lambda calculus includes the following two
rules:
Redo the previous exercise making use of the η-rule whenever possible.
What value is there in the α-rule?
(a) Let true be the name of the lambda expression λx.λy.x and false be
the name of the lambda expression λx.λy.y. Show that ((true E1 ) E2 ) ⇒
E1 and ((false E1 ) E2 ) ⇒ E2 . Define lambda expressions not, and,
and or that behave like their Boolean operation counterparts.
(b) Let 0 be the name of the lambda expression λx.λy.y, 1 be the name
of the lambda expression λx.λy.(x y), 2 be the name of the lambda
expression λx.λy.(x (x y)), 3 be the name of the lambda expression
λx.λy.(x; (x (x y))), and so on. Prove that the lambda expression
succ defined as λz.λx.λy.(x ((z x) y)) rewrites a number to its suc-
cessor.
(a) Show that for any expression E, there exists an expression W such
that (Y E) ⇒ (W W ), and that (W W ) ⇒ (E (W W )). Hence,
(Y E) ⇒ E(E(E(...E(W W )...)))
102 CHAPTER 7. FUNCTIONAL PROGRAMMING
(b) Using the lambda expressions that you defined in the previous parts
of this exercise, define a recursive lambda expression add that per-
forms addition on the numbers defined earlier, that is, ((add m) n) ⇒
m + n.
Logic Programming
Keywords and phrases: Horn clause, Logic programming, inference, modus po-
nens, modus tollens, logic variable, unification, unifier, most general unifier,
occurs-check, backtracking, closed world assumption, meta programming, pat-
tern matching.
A logic program consists of a set of axioms and a goal statement. The rules of
inference are applied to determine whether the axioms are sufficient to ensure
the truth of the goal statement. The execution of a logic program corresponds
to the construction of a proof of the goal statement from the axioms.
S0 × S1 × ... × Sn
103
104 CHAPTER 8. LOGIC PROGRAMMING
The squaring function for natural numbers may be written as a set of tuples as
follows:
{(0, 0), (1, 1), (2, 4)...}
Such a set of tuples is called a relation and in this case the tuples define the
squaring relation.
sqr = {(0, 0), (1, 1), (2, 4)...}
Abstracting to the name sqr and generalizing an individual tuple we can define
the squaring relation as:
sqr = (x, x2 )
Parameterizing the name gives:
sqr(X, Y ) ← Y is X ∗ X
sqr(X,Y) ← Y is X*X.
Note that the set of tuples is named sqr and that the parameters are X and Y.
Prolog does not evaluate its arguments unless required, so the expression Y is
X*X forces the evaluation of X*X and unifies the answer with Y. The Prolog
code
P ← Q.
The logical reading of this rule is as follows: “for every X,Y and Z, X is the uncle
of Z, if X is a male who has a sibling Y which is the parent of Z.” Alternately,
“X is the uncle of Z, if X is a male and X is a sibing of Y and Y is a parent of
Z.”
f(X,Y) ← Y = X*3+4
is an abreviation for
∀X, Y (f (X, Y ) ← Y = X ∗ 3 + 4)
which asserts a condition that must hold between the corresponding domain and
range elements of the function. In contrast, a functional definition introduces a
functional object to which functional operations such as functional composition
may be applied.
f if a0 , ..., am .
g if f, b0 , ..., bn .
the fact
g if a0 , ..., am , b0 , ..., bn .
can be derived.
8.2 Syntax
There are just four constructs: constants, variables, function symbols, predicate
symbols, and two logical connectives, the comma (and) and the implication
106 CHAPTER 8. LOGIC PROGRAMMING
symbol.
P ∈ Programs
C ∈ Clauses
Q ∈ Queries
A ∈ Atoms
T ∈ Terms
X ∈ Variables
8.3 Semantics
sum([Nth],Nth).
sum([Ith|Rest],Ith + Sum Rest) ← sum(Rest,Sum Rest).
8.3. SEMANTICS 107
A proof of its correctness is trivial since the logic program is but a statement of
the mathematical properties of the sum.
PN
{A[N ] = i=N A[i]}
sum([A[I], ..., A[N ]], A[I] + S) ← sum([A[I + 1], ..., A[N ]], S).
Operational Semantics
a.
b ← a.
b?
parent of(a,b).
parent of(b,c).
ancestor of(Anc,Desc) ← parent of(Anc,Desc).
ancestor of(Anc,Desc) ← parent of(Anc,Interm) ∧
ancestor of(Interm,Desc).
parent of(a,b)?
ancestor of(a,b)?
ancestor of(a,c)?
ancestor of(X,Y)?
Consider the query ‘ancestor of(a,b)?’. To answer the question “is a an ancestor
of b”, we must select the second rule for the ancestor relation and unify a with
108 CHAPTER 8. LOGIC PROGRAMMING
Anc and b with Desc. Interm then unifies with c in the relation parent of(b,c).
The query, ancestor of(b,c)? is answered by the first rule for the ancestor of
relation. The last query is asking the question, “Are there two persons such
that the first is an ancestor of the second.” The variables in queries are said to
be existentially quantified. In this case the X unifies with a and the Y unifies
with b through the parent of relation. Formally,
Definition 8.1 A unifier of two terms is a substitution making the terms iden-
tical. If two terms have a unifier, we say they unify.
For example, two identical terms unify with the identity substitution. con-
cat([1,2,3],[3,4],List) and concat([X|Xs],Ys,[X|Zs]) unify with the substitutions
{X = 1, Xs = [2,3], Ys = [3,4], List = [1|Zs]}
There is just one rule of inference which is resolution. Resolution is much like
proof by contradiction. An instance of a relation is “computed” by constructing
a refutation. During the course of the proof, a tree is constructed with the
statement to be proved at the root. When we construct proofs we will use the
symbol ¬ to mark formulas which we either assume are false or infer are false
and the symbol 2 for contradiction. Resolution is based on the inference rule
modus tollens and unification. This is the modus tollens inference rule.
From ¬B
and B ← A0 , ..., An
infer ¬A0 or...or ¬An
Notice that as a result of the inference there are several choices. Each ¬Ai is a
formula marking a new branch in the proof tree. A contradiction occurs when
both a formula and its negation appear on the same path through the proof
tree. A path is said to be closed when it contains a contradiction otherwise a
path is said to be open. A formula has a proof if and only if each path in the
proof tree is closed. The following is a proof tree for the formula B under the
hypothesises A0 and B ← A0 , A1 .
1 From ¬B
2 and A0
3 and B ← A0 , A1
4 infer ¬A0 or ¬A1
5 choose ¬A0
6 contradiction 2
7 choose ¬A1
8 no further possibilities open
There are two paths through the proof tree, 1-4, 5, 6 and 1-4, 7, 8. The first
path contains a contradiction while the second does not. The contradiction is
marked with 2.
8.3. SEMANTICS 109
where identifiers beginning with lower case letters designate constants and iden-
tifiers beginning with an upper case letter designate variables. We can infer that
ogden is an ancestor of mikko as follows.
Notice that all choices result in contradictions and so this proof tree is a proof of
the proposition that ogden is an ancestor of mikko. In a proof, when unification
occurs, the result is a substitution. In the first branch of the previous example,
the term anthonoy is unified with the variable X and anthony is substituted
for all occurences of the variable X.
The unification algorithm can be defined in Prolog. Figure 8.1 contains a formal
definition of unification in Prolog Unification subsumes
• single assignment
• parameter passing
• record allocation
• read/write-once field-access in records
a←b∧c.
110 CHAPTER 8. LOGIC PROGRAMMING
unify(X,Y) ← X == Y.
unify(X,Y) ← var(X), var(Y), X=Y.
unify(X,Y) ← var(X), nonvar(Y), \+ occurs(X,Y), X=Y.
unify(X,Y) ← var(Y), nonvar(X), \+ occurs(Y,X), Y=X.
unify(X,Y) ← nonvar(X), nonvar(Y), functor(X,F,N), functor(Y,F,N),
X =..[F|R], Y =..[F|T], unify lists(R,T).
occurs(X,Y) ← X==Y.
occurs(X,T) ← functor(T,F,N), T =..[F|Ts], occurs list(X,Ts).
A1 ← B., ?- A1 , A2 ,...,An .
?- B, A2 ,...,An .
?- true, A1 , A2 ,...,An .
?- A1 , A2 ,...,An .
resolved([]).
resolved(Goals) ← select(Goal,Goals,RestofGoals),
% Goal unifies with head of some rule
clause(Head,Body), unify( Goal, Head ),
add(Body,RestofGoals,NewGoals),
resolved(NewGoals).
prove(true).
prove((A,B)) ← prove(A), prove(B). % select first goal
prove(A) ← clause(A,B), prove(B). % select only goal and find a rule
b←d.
b←e.
?- a .
By applying the inference rules to the program we derive the following additional
queries:
?- b∧c.
?- d∧c.
?- e ∧ c.
?- c.
?-
Among the queries is an empty query. The presence of the empty query indicates
that the original query is satisfiable, that is, the answer to the query is yes.
Alternatively, the query is a theorem, provable from the given facts and rules.
An interpreter for pure Prolog can be written in Prolog. Figure 8.3 is the
Prolog code for an interpreter.
The interpreter can be used as the starting point for the construction of a
112 CHAPTER 8. LOGIC PROGRAMMING
debugger for Prolog programs and a starting point for the construction of an
inference engine for an expert system.
Declarative Semantics
Definition 8.3 The Herbrand base, denoted by B(P ), is the set of all ground
goals that can be formed from the predicates in P and the terms in the Herbrand
universe.
An interpretation assigns truth and falsity to the elements of the Herbrand base.
A goal in the Herbrand base is true with respect to an interpretation if it is a
member of it, false otherwise.
Denotational Semantics
P ∈ Programs
C ∈ Clauses
Q ∈ Queries
T ∈ Terms
A ∈ Atoms
X ∈ Variables
P ::= (C | Q)...
C ::= G [ ← G1 [ ∧ G2 ]...] .
G ::= A [ ( T [,T]... ) ]
T ::= X | A [ ( T [,T]... ) ]
Q ::= G [,G]... ?
Semantic Domains:
β ∈ B = Bindings
∈ E = Environment
Semantic Functions:
R ∈ Q → B → B + (B × { yes ) + { no }
U∈ C × C → B → B
Semantic Equations:
R[[G]]β, = β0
where
G0 ∈ , U[[G, G0 ]]β = β 0
R[[G]]β, = R[[B]]β 0 ,
where
(G0 ← B) ∈ , U[[G, G0 ]]β = β 0
R[[ G ]]β, = no
where no other rule applies
The logical variable, terms and lists are the basic data structures in logic pro-
gramming.
Here is a definition of the relation between the prefix and suffixes of a list. The
relation is named concat because it may be viewed as defining the result of
appending two lists to get the third list.
concat([ ],[ ])
concat([H|T],L,[H|TL]) ← concat(T,L,TL)
concat(L1,L2,L)
prefix(Pre,L) ← concat(Pre, ,L).
sufix(Suf,L) ← concat( ,Suf,L).
split(L,Pre,Suf) ← concat(Pre,Suf,L).
member(X,L) ← concat( ,[X| ],L).
8.4. THE LOGICAL VARIABLE 115
There two simple types of constants, string and numeric. Arrays may be repre-
sented as a relation. For example, the two dimensional matrix
mary 18.47
data = john 34.6
jane 64.4
may be written as
data(1,1,mary) data(1,2,18.47)
data(2,1,john) data(2,2,34.6)
data(3,1,jane) data(3,2,64.4)
Records may be represented as terms and the fields accessed through pattern
matching.
book(A,T,pub(W),D)
Lists are written between brackets [ and ], so [ ] is the empty list and [b, c] is
the list of two symbols b and c. If H is a symbol and T is a list then [H|T ] is
a list with head H and tail T . Stacks may then be represented as a list. Trees
may be represented as lists of lists or as terms.
Lists may be used to simulate stacks, queues and trees. In addition, the logical
variable may be used to implement incomplete data structures.
The following code implements a binary search tree as an incomplete data struc-
ture. It may be used both to construct the tree by inserting items into the tree
and to search the tree for a particular key and associated data.
116 CHAPTER 8. LOGIC PROGRAMMING
lookup(Key,Data,bt(Key,Data,LT,RT))
lookup(Key,Data,bt(Key0,Data0,LT,RT)) ← Key @< Key0,
lookup(Key,Data,LT)
lookup(Key,Data,bt(Key0,Data0,LT,RT)) ← Key @> Key0,
lookup(Key,Data,RT)
This is a sequence of calls. Note that the initial call is with the variable BT .
The first three calls initialize the dictionary to contain those entries while the
last call extracts janes’s age from the dictionary.
The logical and the incomplete data structure can be used to append lists in
constant time. The programming technique is known as difference lists. The
empty difference list is X/X. The concat relation for difference lists is defined
as follows:
?- concat_dl([1,2,3|X]/X,[4,5,6|Y]/Y,Z).
_X = [4,5,6 | _11]
_Y = _11
_Z = [1,2,3,4,5,6 | _11] / _11
Yes
The relation between ordinary lists and difference lists is defined as follows:
Arithmetic
Terms are simply patterns they may not have a value in and of themselves.
For example, here is a definition of the relation between two numbers and their
product.
times(X,Y,X×Y)
8.5. ITERATION VS RECURSION 117
However, the product is a pattern rather than a value. In order to force the
evaluation of an expression, a Prolog definition of the same relation would be
written
times(X,Y,Z) ← Z is X×Y
Not all recursive definitions require the runtime support usually associated with
recursive subprogram calls. Consider the following elegant mathematical defi-
nition of the factorial function.
1 if n = 0
n! =
n × (n − 1)! if n > 0
Here is a direct restatement of the definition in a relational form.
factorial(0,1)
factorial(N,N×F) ← factorial(N-1,F)
In Prolog this definition does not evaluate either of the expressions N-1 or N×F
thus the value 0 will not occur. To force evaluation of the expressions we rewrite
the definition as follows.
factorial(0,1)
factorial(N,F)← M is N-1, factorial(M,Fm), F is N×Fm
Note that in this last version, the call to the factorial predicate is not the last
call on the right-hand side of the definition. When the last call on the right-
hand side is a recursive call (tail recursion) then the definition is said to be an
iterative definition. An iterative version of the factorial relation may be defined
using an accumulator and tail recursion.
fac(N,F) ← fac(N,1,F)
fac(0,F,F)
fac(N,P,F) ← NP is N×P, M is N-1, fac(M,NP,F)
In this definition, there are two different f ac relations, the first is a 2-ary rela-
tion, and the second is a 3-ary relation.
rev(L, R) ← rev(L, [ ], R)
rev([ ], R, R)
rev([H|T ], L, R) ← rev(T, [H|L], R)
8.6 Backtracking
When there are multiple clauses defining a relation it is possible that either some
of the clauses defining the relation are not applicable in a particular instance or
that there are multiple solutions. The selection of alternate paths during the
construction of a proof tree is called backtracking.
8.7 Exceptions
Prolog is not logic programming. The execution model for Prolog omits the oc-
curs check, searches the rules sequentially, and selects goals sequentially. Back-
tracking, infinite search trees ...
Prolog
P ∈ Programs
C ∈ Clauses
Q ∈ Query
H ∈ Head
B ∈ Body
A ∈ Atoms
T ∈ Terms
X ∈ Variable
Incompleteness
p( a, b ).
p( c, b ).
p( X, Z ) ← p( X, Y ), p( Y, Z).
p( X, Y ) ← p( Y, X ).
?- p( a, c ).
The result is an infinite loop. The first and fourth clauses imply p( b, c ). The
first and third clauses with the p( b, c) imply the query. Prolog gets lost in an
infinite branch no matter how the clauses are ordered, how the literals in the
bodies are ordered or what search rule with a fixed order for trying the clauses
is used. Thus logical completeness requires a breadth-first search which is too
inefficient to be practical.
Unfairness
concat( [ ], L, L ).
concat( [H|L1], L2, [X|L] ) ← concat( L1, L2, L ).
concat3( L1, L2, L3, L ) ← concat( L1, L2, L12 ),
concat( L12, L3 L ).
?- concat3( X, Y, [2], L).
Unsoundness
test ← p( X, X ).
p( Y, f( Y )).
?- test.
Lacking the occur check Prolog will succeed but test is not a logical consequence
of the logic program.
concat( [ ], L, L ).
concat( [H|L1], L2, [X|L] ) ← concat( L1, L2, L ).
?- concat( [ ], L, [1|L] ).
In this instance Prolog will succeed (with some trouble printing the answer).
There are two solutions, the first is to change the logic and permit infinite terms,
the second is to introduce the occur check with the resulting loss of efficiency.
Negation
p( a ).
r( b ) ← not p( Y ).
?- not p(b).
The goal succeeds but is not a logical consequence of the logic program.
q( a ) ← r( a ).
q( a ) ← not r( a ).
r( X ) ← r( f( X ) ).
?- q ( a ).
122 CHAPTER 8. LOGIC PROGRAMMING
The query is a logical consequence of the first two clauses but Prolog cannot
determine that fact and enters an infinite derivation tree. However the closed
world assumption is useful from a pragmatic point of view.
Control Information
a(1).
a(2).
a(3).
p ← a(I),!,print(I),nl,fail.
?- p.
1
No
Extralogical Features
The extralogical primitives bagof, setof, assert, and retract are outside the
scope of first-order logic but are useful from the pragmatic point of view.
In Prolog there are builtin predicates to test for the various syntactic types,
lists, numbers, atoms, clauses. Some predicates which are commonly available
are the following.
var(X) X is a variable
atomic(A) A is an atom or a numeric constant
functor(P,F,N) P is an N-ary predicate with functor F
clause(Head,Body) Head ← Body is a formula.
bagof(X,P,B), setof(X,P,B)
trace(Q) ← trace1([Q])
trace1([])
trace1([true|R]) ← !, trace1(R).
trace1([fail|R]) ← !, print(’< ’), print(fail), nl, fail.
trace1([B|R]) ← B =..[’,’|BL], !, concat(BL,R,NR), trace1(NR).
trace1([F|R]) ← builtin(F),
print(’> ’), print([F|R]), nl,
F,
trace1(R),
print(’<’), print(F), nl
trace1([F|R]) ← clause(F,B),
print(’>’), print([F|R]),nl,
trace1([B|R]),
print(’< ’), print(F), nl
trace1([F|R]) ← \+ builtin(F), \+ clause(F,B),
print(’> ’), print([F|R]),nl,
print(’< ’), print(F), print(’ ’), print(fail), nl, fail
Multidirectionality
Computation of the inverse function must be restricted for efficiency and unde-
cidability reasons. For example consider the query
?- factorial(N,5678).
An implementation must either generate and test possible values for N (which
is much too inefficient) or if there is no such N the undecidability of first-order
logic implies that termination may not occur.
Rule Order
Rule order affects the order of search and thus the shape of the proof tree. In
the following program
124 CHAPTER 8. LOGIC PROGRAMMING
concat([ ],L,L).
concat([H|T],L,[H|R]) ← concat(T,L,R).
?- concat(L1,[2],L).
L1 = [ ], L = [2]
L1 = [V1], L = [V1,2]
L1 = [V1,V2], L = [V1,V2,2]
...
concat([H|T],L,[H|R]) ← concat(T,L,R).
concat([ ],L,L).
?- concat(L1,[2],L).
then the execution fails to terminate, entering an infinite loop since the first
rule is always applicable.
A database consists of one or more relations. The data stored in the relations is
manipulated using commands written in a query language. The operations
provided the query language include union, set difference, cartesian product,
projection, and selection.
8.10. LOGIC PROGRAMMING VS FUNCTIONAL PROGRAMMING 125
History
Kowalski’s paper[16]
Logic programming techniques
Implementation of Prolog
SQL
DCG
8.12 Exercises
1. Modify concat to include an explicit occurs check.
2. Construct a family data base f db(f,m,c,sex) and define the following re-
lations, f of, m of, son of, dau of, gf, gm, aunt, uncle, ancestor, half sis,
half bro.
3. Business Data base
4. Blocks World
5. CS Degree requirements; course(dept,name,prereq). don’t forget w1 and
w2 requirements.
6. Circuit analysis
7. Tail recursion
8. Compiler
9. Interpreter
10. Tic-Tac-Toe
11. DCG
12. Construct Prolog analogues of the relational operators for union, set dif-
ference, cartesian product, projection and selection.
13. Airline reservation system
126 CHAPTER 8. LOGIC PROGRAMMING
Chapter 9
Imperative Programming
127
128 CHAPTER 9. IMPERATIVE PROGRAMMING
The following examples illustrate the general form for variable declarations in
imperative programming languages.
−−Ada
V := E;
Modula-2
V := E;
//C
V = E;
APL
V ←
; Scheme (setq V E)
Aside: The use of the symbol (=) in C confuses the distinction between
definition, equality and assignment. The symbol (=) is used in mathematics in
two distinct ways. It is used to define and to assert the equality between two
values. In C it neither means define nor equality. In C the symbol (==) is used
for equality, while the form: Type Variable is used for definitions. 2
X := 3;
X := X + 1
are understood as follows: assign X to three and then reassign X to the value of
the expression X+1 which is four. Thus, after the sequence of assignments, the
value of X is four.
From the point of view of axiomatic semantics, the assignment the a predicate
transformer. It is a function from predicates to predicates. From the point
of view of denotational semantics, the assignment is a function from states to
states. From the point of view of operational semantics, the assignment changes
the state of an abstract machine.
Since the assignment can assign names to new values, the order in which as-
signments are executed is important. Control structures are syntactic structures
that allow assignments to be combined in useful ways.
Skips
Procedure Calls/Coroutines
Parameter passing
Composition
Alternation
-- Ada
132 CHAPTER 9. IMPERATIVE PROGRAMMING
if condition then
commands
{ elsif condition then
commands }
[ else
commands]
endif
case expression is
when choice {| choice} => commands
{when choice {| choice} => commands}
[when others => commands]
end case;
Iteration
[ loop name: ]
[ iteration scheme ]
loop
commands
end loop [ loop name ]
[ loop name: ]
while condition loop
commands
end loop [ loop name ]
[ loop name: ]
for identifier in [ reverse ] descrete range loop
9.2. CONTROL STRUCTURES 133
commands
end loop [ loop name ]
[ loop name: ]
for identifier in [ reverse ] descrete range loop
...
exit [ loop name ] [ when condition];
...
end loop [ loop name ]
An iterator is used with the an extended form of the for loop where the iterator
replaces the initial and final values of the loop index. For example, given a binary
search tree and a generator which performs inorder tree traversal, an iterator
would iterate for each item in the tree following the inorder tree traversal.
etc
In Prolog ... the form ... generator(P) ... fail . Backtracking produces the
successive elements of the generator.
134 CHAPTER 9. IMPERATIVE PROGRAMMING
% Generators
% Natural Numbers
nat(0).
nat(N) :- nat(M), N is M + 1.
inf(I,I).
inf(I,N) :- I1 is I+1, inf(I1,N).
% Sequence of Squares
inf(I,J,I) :- I =< J.
inf(I,J,N) :- I < J, I1 is J + (J-I), inf(J,I1,N).
inf(I,J,I) :- I > J.
inf(I,J,N) :- I > J, I1 is J + (J-I), inf(J,I1,N).
between(I,J,I) :- I =< J.
between(I,J,N) :- I < J, I1 is I+1, between(I1,J,N).
between(I,J,I) :- I > J.
between(I,J,N) :- I > J, I1 is I-1, between(I1,J,N).
between(I,J,K,I) :- I =< K.
between(I,J,K,N) :- I < K, J1 is J + (J-I), between(J,J1,K,N).
between(I,J,K,I) :- I > K.
between(I,J,K,N) :- I > K, J1 is J + (J-I), between(J,J1,K,N).
inflist(N,[N]).
inflist(N,[N|L]) :- N1 is N+1, inflist(N1,L).
% List of Primes
sieveP(P,[],[]).
sieveP(P,[N|L],[N|IDL]) :- N mod P > 0, sieveP(P,L,IDL).
sieveP(P,[N|L], IDL) :- N mod P =:= 0, L \= [], sieveP(P,L,IDL).
last([N],N).
last([H|T],N) :- last(T,N).
ssieve([P],P).
ssieve([P|L],NP) :- L \= [], sieveP(P,L,PL), ssieve(PL,NP).
9.3 Sequencers
In Ada, the exit sequencer terminates an enclosing loop. All enclosing loops
upto and including the named loop are exited and execution follows with the
command following the named loop.
136 CHAPTER 9. IMPERATIVE PROGRAMMING
Ada uses the return sequencer to terminate the execution of the body of a
procedure or function and in the case of a function, to return the result of the
computation.
Exception handers are sequencers that take control when an exception is raised.
• Jumps
• Exits
• Exceptions – propagation, raising, resumption, handler (implicit invoca-
tion)
• Coroutines
The machine languge of a typical computer includes instructions which allow any
instruction to be selected as the next instruction. A sequencer is a construct that
is provided to give high-level programming languages some of this flexibilty. We
consider three sequencers, jumps, escapes, and exceptions. The most powerful
sequencer (the goto) is also the most criticized. Sequencers can make it difficult
to understand a program by producing ‘spaghetti’ like code. So named because
the control seems to wander around in the code like the strands of spaghetti.
9.4 Jumps
goto L
if conditional expression goto L
At the machine level alternation and iteration may be implmented using labels
and goto commands. Goto commands often take two forms:
goto LABELi
The sequence of instructions next executed begin with the command la-
beled with LABELi .
9.5. ESCAPE 137
The goto commands have a number of advantages, they have direct hardware
support and are completely general purpose. There are also a number of disad-
vantages, programs are flat without hierarchical structure and the code may be
difficult to read and understand.
9.5 Escape
return expr
is often used to exit a function call and return the value computed by the
function.
exit(n)
is used to exit n enclosing constructs. The exit command can be used in con-
junction with a general loop command to produce while and repeat as well as
more general looping constructs.
In C a break command sends control out of the enclosing loop to the com-
mand following the loop while the continue command transfers control to the
beginning of the enclosing loop.
138 CHAPTER 9. IMPERATIVE PROGRAMMING
9.6 Exceptions
There are many “exception” conditions that can arise in program execution.
Some exception conditions are normal for example, the end of an input file
marks the end of the input phase of a program. Other exception conditions
are genuine errors for example, division by zero. Exception handlers of various
forms can be found in PL/1, ML, CLU, Ada, Scheme and other languages.
There are two basic types of exceptions which arise during program execution.
They are domain failure, and range failure.
Definition 9.4 The action to resolve the exception is called handling the ex-
ception. The propagation of an exception is the passing of the exception to the
context where it can be handled.
The next level of error handling is to return a value outside the range of the
operation. This could be a global variable, a result parameter or a function
result. This approach requires explicit checking by the programmer for the
error values. For example, the eof boolean is set to true when the program
has read the last item in a file. The eof condition can then be checked before
9.6. EXCEPTIONS 139
Responses to an Exception
Issues
Once an exception has been detected, control is passed to the handler that de-
fines the action to be taken when the exception is raised. The question remains,
what happens after handling the exception?
Some exceptions are inefficient to implement (for example, run time range checks
on array bounds). The such exceptions are usually implemented in software
and may require considerable implementation overhead. Some languages give
the programmer control over whether such checks and the raising of the cor-
responding exception will be performed. This permits the checks to be turned
on during program development and testing and then turned off for normal
execution.
1. Handler Specification
2. Default Handlers
3. Propagation of Exception
140 CHAPTER 9. IMPERATIVE PROGRAMMING
Prolog, Icon
9.7 Coroutines
Multiple threads of control but control is passed from thread to thread under
the active thread’s control.
9.8 Processes
it will be false.
As another example of side effects, consider the C function getint as used in the
following two expressions.
9.10. ALIASING 141
2 * getint ()
getint () + getint ()
The two expressions are different. The first multiplies the next integer read
from the input file by two while the second expression denotes the sum of the
next two successive integers read from the input file. And yet as mathematical
expressions they should denote the same value.
Side effects are a feature of imperative programming languages that make rea-
soning about the program difficult. Side effects are used to provide communi-
cation among program units. When undisciplined access to global variables is
permitted, the program becomes difficult to understand. The entire program
must be scanned to determine which program units access and modify the global
variables since the call command does not reveal what variables may be affected
by the call.
9.10 Aliasing
Two variables are aliases if they denote (share) the same data object during
a unit activation. Aliasing is another feature of imperative programming lan-
guages that makes programs harder to understand and harder to reason about.
x := a + b
y := c + d
But suppose x and c are aliases for the same object. In this case, the assign-
ments are interdependent and the order of evaluation is important. Often in
the optimization of code it is desireable to reorder steps or to delete unecessary
steps. This cannot be done when aliasing is possible.
Aliasing can arise from variable definitions and from variable parameters. Con-
sider calls confuse( i, i) and confuse( a[i], a[j] ) given the following
142 CHAPTER 9. IMPERATIVE PROGRAMMING
Pascal procedure
in the first call i is set to 2. The result of the second call depends on the values
of i and j. The second call shows that the detection of aliasing may be delayed
until run time. No compiler can detect aliasing in general.
• A formal parameter and a global variable denote the same data object.
var p, q : ↑ T;
...
new(p);
q := p
Dangling References
procedure Dangling;
var q : Pointer;
begin;
new(q); q^ := 23; p := q; dispose(q)
end;
begin
new(p); Dangling(p)
end;
i := 1;
while (i <= length) and (list[i] <> value) do i := i+1
144 CHAPTER 9. IMPERATIVE PROGRAMMING
command := SKIP
| IDENT := exp
| IF guarded command alternative ... FI
| DO guarded command alternative ... OD
| command; command
guarded command := guard → command
alternative := [] guarded command
The code implements a sequential search for a value in a table and terminates
when either the entire table has been searched or the value is found. Assuming
that the subscript range for list is 1 to length it seems reasonable that the
termination of the loop should occur either when the index is out of bounds or
when the value is found. That is, the arguments to the and should be evaluated
sequentually and if the first argument is false the remaining argument need not
be evaluated since the value of the expression cannot be true. Such an evaluation
scheme is call short-circuit evaluation.
In most implementations, if the value is in the list, the program aborts with a
subscript out of range error.
The Ada language provides the special operators and then and or else so that
the programmer can specify short-circuit evaluation.
The value of E0 ;E1 the value of E1 . E0 is evaluated for its side-effect and the
value is discarded. There is no obvious result for iterative control structures.
Typically they are defined to return zero or a null value.
Exercises
1. Give all possible forms of assignment found in the programming language
C.
7. Goto elimination
8. Axiomatic semantics
9. Denotational semantics
146 CHAPTER 9. IMPERATIVE PROGRAMMING
Concurrent Programming
Operations are sequential, if they occur one after the other, ordered in time.
Operations are concurrent, if they overlap in time. Operations in the source
text of a program are concurrent if they could be, but need not be, executed
in parallel. Concurrent programming involves the notations for expressing po-
tential parallelism and the techniques for solving the resulting synchronization
and communication problems. Notations for explicit concurrency are a pro-
gram structuring technique while parallelism is mode of execution provided by
the underlying hardware. Thus we can have parallel execution without explicit
concurrency in the language. This is the case when functional and logic pro-
gramming languages are executed on appropriate hardware or a program is
compiled by a parallelizing compiler. We can have concurrency in a language
without parallel execution. This is the case when a program (with or without
explicit concurrent sections) is executed on a single processor. In this case, the
program is executed by interleaving executions of concurrent operations in the
source text.
147
148 CHAPTER 10. CONCURRENT PROGRAMMING
PL/1
ALGOL 68
Ada
SR
10.1 Concurrency
Interrupts and the hardware clock also made possible the development of inter-
active systems where multiple users have simultaneous access to the system re-
sources. Such a system must provide for a large number of jobs whose combined
demands on the system may exceed the system resources. Various techniques of
swapping and paging meet this need by moving jobs in and out of the store to
the larger capacity of backing store devices. With the increase of jobs comes the
need to increase the capacity of the CPU. The solution was to develop multipro-
cessor systems in which several CPUs are available and simultaneously operate
on separate jobs in the shared store.
Other advances in hardware have lead to the the development of alternative ar-
10.1. CONCURRENCY 149
chitectures. Pipeline processors which fetch the next instruction while the first
instruction is being decoded. Array processors provide a large number of iden-
tical processors that operate simultaneously on different parts of the same data
structure. Data flow computers aim at extracting maximum concurrency from
a computation by performing as much of the computation in parallel as possi-
ble. Connectionism based hardware models provide concurrency by modeling
computation after the neural networks found in the brain.
Aside: The terms, concurrent, distributed and parallel have a been used at
various times to describe various types of concurrent programming. Multiple
processors and disjoint or shared store are implementation concepts and are not
important from the programming language point of view. What matters is the
notation used to indicate concurrent execution, communication and synchro-
nization. 2
The basic correctness issues in the design of concurrent programs are safety and
liveness.
• Safety: nothing bad will happen. For example, access to a shared resource
like a printer requires that the user process have exclusive access to the
resource. So there must be a mechanism to provide mutual exclusion.
• Liveness: something good will happen. On the other hand, no process
should prevent other processes from eventual access to the printer. Thus
any process which wants the printer must eventually have access to the
printer.
A safety property for this problem is that a fork is held by one and only one
philosopher at a time. A desireable liveness property is that whenever a philoso-
pher wants to eat, eventually the philosopher will get to eat.
Nondeterminism
Communication
Two processes are said to communicate if an action of one process must entirely
precede an action of a second process.
Synchronization
Mutual Exclusion
Often a process must have exclusive access to a resource. For example, when a
process is updating a data structure, no other process should have access to the
same data structure otherwise the accuracy of the data may be in doubt. The
necessity to restrict access is termed mutual exclusion and involves the following:
There are several solutions to the mutual exclusion problem. Among the solu-
tions are semiphores, critical regions and monitors.
Scheduling
When there are active requests for a resource there must be a mechanism for
granting the requests. Often a solution is to grant access on a first-come, first-
served basis. This may not always be desireable since there may be processes
152 CHAPTER 10. CONCURRENT PROGRAMMING
whose progress is more important. Such processes may be given a higher pri-
ority and their requests are processed first. When processes are prioritized,
some processes may be prevented from making progress (such a process is live-
locked). A fair scheduler insures that all processes eventually make progress
thus preventing live-lock.
Deadlock
Deadlock can occur in a system of processes and resources if, and only if, the
following conditions all hold together.
• Wait and hold: processes continue to hold a resource while waiting for a
new resource request to be granted.
A common approach is to ignore deadlock and hope that it will not happen.
If deadlock occurs, (much as when a program enters an infinite loop) the sys-
tem’s operators abort the program. This is not an adequate solution in highly
concurrent systems where reliablility is required.
A second approach is to allow deadlocks to occur but detect and recover auto-
matically. Once deadlock is detected, processes are selectively aborted or one or
more processes are rolled back to an earlier state and temporarily suspended un-
til the danger point is passed. This might not an acceptable solution in real-time
systems.
10.3 Syntax
[P1 k P2 k ... k Pn ]
Pi !E, Pj ?X
wait(Pi ), signal(Pj )
Processes that access a common address space may interfere with each other.
In this program,
[i := 1 k i := 2]
[i := 0; i := i + 1 k i := 2]
Processes which have disjoint address spaces cannot interfere with each other
and thus can operate without fear of corrupting each other. For example, the
two processes in
[i := 1 k j := 2]
do not share an address space therefore, the assignments may take place in
parallel.
The requirement for disjoint address space may be too severe a requirement.
What is required is that shared resources may need to be protected so that only
one process is permitted access to the resourse at a time. This permits processes
to cooperate, sharing the resource but maintaining the integrity of the resource.
10.7. SYNCHRONIZING PROCESSES 155
semiphore
critical section
monitor
Since the processes access different portions of the queue and test for the pres-
ence or absence of items in the queue before accessing the queue, the desired
safety properties are satisfied. Note however, that busy waiting is involved.
• Semaphores
• Critical Regions
• Monitors
• Synchronized Message Passing
Note that busy waiting is still involved and further once a process is in the mon-
itor and is waiting, no other process can get in and the program is deadlocked.
Livelock may result if there are more than one waiting process and when the
signal is received access is not granted fairly.
Starvation: (livelock) multiple processes waiting for access but access is not
provided in a fair manner
Coroutines.
In the previous solution, it was assumed that the processes shared the address
space and that synchronization was achieved by the use of monitor and con-
dition queues. If the address spaces are disjoint, then both communication
and synchronization must be achieved through message passing. There are two
choices, message passing can be synchronous or asynchronous. When message
158 CHAPTER 10. CONCURRENT PROGRAMMING
process Q;
const qsize = 10;
var head, tail : integer;
queue : array[0..qsize-1] of integer;
begin
head,tail := 0,0;
*[ head 6= tail, C?X → C!queue[head]; head := (head + 1) mod qsize
2 head 6= (tail+1) mod qsize, P?X → queue[tail],tail := X, (tail + 1) mod qsize]
end;
process P;
begin
*[ true → produce(X); Q!X]
end;
process C;
begin
*[ true → Q!X, Q?X; consume(X)]
end;
begin
[PkCkQ]
end.
10.9 Occam
10.10 Semantics
dent.
2. Communication-exchange of information
3. Scheduling-priority,
Languages which have been designed for concurrent execution include Concur-
rent Pascal, Ada and Occam. Application areas are typically operating systems
and distributed processing.
Ensemble activity
10.12 Examples
Pipelines
Systolic arrays
Dining Philosophers
Exercises
1. independent
2. pipe line
160 CHAPTER 10. CONCURRENT PROGRAMMING
3. synchronized
Chapter 11
PCN
11.1 Tutorial
Program Composition
Getting Started
Choice Composition
Stream Communication
161
162 CHAPTER 11. PCN
11.3 Examples
Exercises
Chapter 12
Abstraction and
Generalization II
• Partitions
• Separate compilation
– Linking
– Name and Type consistency
• Scope rules
– Import
– Export
• Modules–collection of objects–definitions
• Package
163
164 CHAPTER 12. ABSTRACTION AND GENERALIZATION II
12.1 Encapsulation
The object part of a definition often contains other definitions which are said
to be local definitions. Such local definitions are not visible or available to be
referenced by other definitions. Thus the object part of a definition involves
“information hiding”. This hidden information is sometimes made available by
exporting the names.
The work of constructing large programs is divided among several people, each
of whom must produce a part of the whole. Each part is called a module and
each programmer must be able to construct his/her module without knowing
the internal details of the other parts. This is only possible when each module
is is separated into an interface part and an implementation part. The
interface part describes all the information required to use the module while
the implementation part describes the implementation. This idea is already
present in most programming languages in the manner in which functions and
procedures are defined. Function and procedure definitions usually are
separated into two parts. The first part gives the subprogram’s name and
parameter requirements and the second part describes the implementation. A
module is a generalization of the concept of abstraction in that a module is
permitted to contain a collection of definitions. An additional goal of modules
is to confine changes to a few modules rather than throughout the program.
Advantages
• reduction in complexity
• team programming
• maintainability
• reusability of code
• project management
Implementation
Typical applications:
12.2 ADTs
name : adt
operation signatures
...
12.3 Partitions
• Procedures
• Functions
• ADTs
• Modules
There are a number of mechanisms for combining the partitions into a single
program for the purposes of compilation and execution. The include
statement is provided in a number of languages. It is a compiler directive with
directs the compiler to textually include the named file in the source program.
In some systems the partitions may be separately compiled and there is a
linking phase in which the compiled program modules are linked together for
execution. In other systems, at run-time any missing function or procedure
results in a run-time search for the missing module which if found is then
executed or if not found results in a run-time error.
The act of partitioning a program raises the issue of the scope of names.
Which objects with in the module are to be visible outside the module? The
usual solution is to designate some names to be exported and others to be
private or local to the module and invisible to other modules. In case there
might be name conflict between exported names from modules, modules are
often permitted to designate names that are to be imported from designated
modules or to qualify the name with the module name.
The scope rules for modules define relationships among the names within the
modules. There are four choices.
12.5 Modules
The what is of concern to the user of the module while the how is of concern to
the implementer of the module.
Exercises
1. Algebraic Semantics: stack, tree, queue, grade book etc
168 CHAPTER 12. ABSTRACTION AND GENERALIZATION II
5. Run-time Stack
Chapter 13
Object-Oriented
Programming
• History
– Simula
– ADT
– Small-Talk
– Modula-2, C++, Eiffel
• Subtypes (subranges)
• Generic types
169
170 CHAPTER 13. OBJECT-ORIENTED PROGRAMMING
• OOP
– Objects–state + operations
– Object Classes– Class, Subclass
• Objects–state + operations
• Object Classes– Class, Subclass
• Inheritance mechanism
Functional objects are like values, imperative objects are like variables, active
objects are like processes.
Definition 13.1
13.1 History
• Simula
• Small-Talk
• Modula-2, C++, Eiffel
• Flavors, CLOS
The subtype principle states that a subtype may appear wherever an element
of the super type is expected.
13.3 Objects
Objects which are collections of functions but which do not have a state are
functional objects. Functional objects are like values, they have the object-like
interface but no identity that persists between changes of state. Functional
objects arise in logic and functional programming languages.
name : object
methods
...
For example,
name : object
variables
...
methods
...
Objects which may be active when a message arrives are active objects. In
contrast, functional and imperative objects are passive unless activated by a
message. Active objects have three modes: when there is nothing to do the
object is dormant, when the agent is executing it is active, and when an object
is waiting for a resource or the completion of subtasks it is waiting. Messages
sent to an active object may have to wait in queue until the object finishes a
task. Message passing among objects may be synchronous or asynchronous.
13.4 Classes
Classes serve as templates from which objects can be created. Classes have the
same instance variables and operations as corresponding objects but their
interpretation is different. Instance variables in an object represent actual
variables while class instance variables are potential, being instantiated only
when an object is created.
name : class
instance variables
...
class variables
...
174 CHAPTER 13. OBJECT-ORIENTED PROGRAMMING
instance methods
...
class methods
...
Classes specify the behavior common to all elements of the class. The
operations of a class determine the behavior while the instance variables
determine the structure.
Algebraic semantics
13.5 Inheritance
Inheritance classifies classes in much the way classes classify values. The
ability to classify classes provides greater classification power and conceptual
modeling power. Classification of classes may be referred to as second-order
classification. Inheritance provides second-order sharing, management, and
manipulation of behavior that complements first-order management of objects
by classes.
name : class
super class
...
instance variables
{ as before }
13.6. TYPES AND CLASSES 175
DYNAMIC/STATIC/INHERITANCE
Type hierarchy
object(structure,methodslist).
isa(type1,type2).
object(rectangle(Length,Width),[area(A is Length*Width)]).
Algebraic semantics
The concept of a type and the concept of a class have much in common and
depending on the point of view, they may be indistinguishable. The
distinction between types and classes may be seen when we examine the
176 CHAPTER 13. OBJECT-ORIENTED PROGRAMMING
compare the inheritance relationship between types and subtypes with the
inheritance relationship between classes and subclasses.
Example 13.1 The natural numbers are a subtype of the integers but while
subtraction is defined for all pairs of integers it is not defined for all pairs of
natural numbers.
Example 13.2 The integers are a subclass of the natural numbers since, the
subtraction operation of the natural numbers can be extended to subtraction for
integers.
Example 13.3 The rational numbers are a subclass of the integers since, they
can be defined as pairs of natural numbers and the arithmetic operations on the
rational numbers can be defined in terms of the arithmetic operations on
natural numbers.
Types are used for type checking while classes are used for generating and
managing objects with uniform properties and behaviors. Every class is a
type. However, not every type is a class, since predicates do not necessaryly
determine object templates. We will use the term type to refer to structure
and values while the term class will be used to refer to behavior.
13.7 Examples
Natural numbers – Ds
Integers – (=-,Ds)
Rationals
Reals – (+-,Ds,Ds)
13.9 Exercises
• Stack
• Queue
• Tree
Pragmatics
14.1 Syntax
In view of abstract syntax it may seem that concrete syntax does not matter.
In fact, details such as placement of keywords, semicolons and case do matter.
Algol-68 and Modula-2 require closing keywords. Modula-2 uses end while
Algol-68 uses the reverse of the opening keyword for example,
14.2 Semantics
179
180 CHAPTER 14. PRAGMATICS
Algebraic semantics are useful for the specification of abstract data types.
For effective use axiomatic semantics require support for program varification.
Operational semantics –
Bindings may occur at various times from the point of language definition
through program execution. The time at which the binding occurs is termed
the binding time.
Early binding often permits more efficient execution of programs while late
binding permits more flexibility. The implementation of recursion may require
allocation of memory at run-time in contrast a one time to allocation of
memory at compile-time.
14.4. VALUES AND TYPES 181
The primitive types are implemented using both the underlying hardware and
special purpose software. So that only appropriate operations are applied to
values, the value’s type must be known. The type information is contained in
a descriptor. When the type of a value is known at compile time the type
descriptor is a part of the symbol table and is not needed at run-time and
therefore, the descriptor is discarded after compilation. When the type a value
is not known until run-time, the type descriptor must be associated with the
value to permit type checking.
Boolean values are implemented using a single bit of storage. Since single
bits are not usually addressable, the implementation is extended to be a single
addressable unit of memory. In this case either a single bit within the
addressable unit is used for the value or a zero value in the storage unit
designates false while any non-zero value designates true.
Integer values are most often implemented using a hardware defined integer
storage representation. The integer arithmetic and relational operations are
implemented using the set of hardware operations. The storage unit is divided
into a sign and a binary number. Since the integers form an infinite set, only a
subrange of integers is provided. Some languages (for example Lisp and
Scheme) provide for a greatly extended range by implementing integers in lists
and providing the integer operations in software. This provides for “infinite”
precision arithmetic.
Natural number values are most often implemented using the hardware
defined storage unit. The advantage of providing an natural number type is
that an additional bit of storage is available thus providing larger positive
values than are provided for integer values.
Real number values are most often implemented using a hardware defined
floating-point representation. The floating-point arithmetic and relational
operations are implemented using the set of hardware operations. Some
floating-point operations such as exponentiation are provided in software. The
storage unit is divided into a mantissa and an exponent. Sometimes more than
one storage unit is used to provide greater precision.
Where strings are treated as a primitive type, they are usually of fixed length
and their operations are implemented in hardware.
Abstract data types are best implemented with pointers. The user program
holds a pointer to a value of the abstract type. This use of pointers is quite
safe since the pointer manipuation is restricted to the implementation module
and the pointer is notationally hidden.
In the discussion which follows, the term subprogram will be used to refer to
whole programs, procedures and functions.
The run time environment must keep track of the current instruction and the
referencing environment for each active or waiting program so that when a
subprogram terminates, the proper instruction and data environment may be
selected for the calling subprogram.
contains links (pointers) which reveal the dynamic history of the program.
When a programming language does not permit recursive procedures and data
structure size is independent of computed or input data, the maximum storage
requirements of the program can be determined at compile time. This
simplifies the run time support required by the program and it is possible to
statically allocate the storage used during program execution.
A variables declared within a block have a lifetime which extends from the
moment an activation record is created for the block until the activation
record for the block is destroyed. A variable is bound to an offset within the
activation record at compile time. It is bound to a specific storage location
when the block is activated and becomes a part of storage.
The static scope rules are implemented as follows. The data section of each
procedure is associated with an activation record. The activation records
are dynamically allocated space on a runtime stack. Each recursive call is
associated with it own activation record. Associated with each activation
record is a dynamic link which points to the previous activation records, a
return address which is the address of the instruction to be executed upon
return from the procedure and a static link which provides access to the
referencing environment.
184 CHAPTER 14. PRAGMATICS
An activation record consists of storage for local variables, the static and
dynamic links and the return address.
SL RA DL
Storage for local data
The runtime stack of activation records (local data, static and dynamic links).
Global data values are found by following the static chain to the appropriate
activation record.
A heap variable is one that can be created and deleted at any time. Heap
variables are anonymous and are accessed through pointers. A heap is a block
of storage within which pieces are allocated and freed in some relatively
unstructured mannner.
Initially the elements of the heap are linked together in some fashion to form a
free-space list. The creation of a heap variable is requested by an operation
14.7. SCOPE AND BLOCKS 185
The heap variable’s lifetime extends from the time it is created until it is no
longer accessable.
There are two stages to garbage collection a marking phase and a collecting
phase.
Marking phase: The marking phase begins outside the heap with the
pointers that point to active heap elements. The chains of pointers are
followed and each heap element in the chain is marked to indicate that it
is active. When this phase is finished only active heap elements are
marked as active.
Collecting phase: Ub the collecting phase the heap is scanned and each
element which is not active is returned to the free-space list. During this
phase the marked bits are reset to prepare for a later garbage collection.
Coroutines
Coroutines are used in discrete simulation languages and, for some problems,
provide a control structure that is more natural than the usual hierarchy of
subprogram calls.
Each activation record is extended to include a location to store the CI for the
corresponding coroutine. It is initialized with the location of the first
instruction of the coroutine. When coroutine encounters a resume operation, it
stores the address of its next instruction in it own activation record. The
address of the CI for the resumed coroutine is obtained from the activation
record of the resumed coroutine.
14.8. PARAMETERS AND ARGUMENTS 187
Concurrency
The programming language Scheme assumes that all functions are strict in
their parameters, therefore, the parameters are evaluated when the function is
called. This evaluation scheme is called eager evaluation. This is not always
desirable and so Scheme provides for the quote operator to inform a function
not to evaluate its parameters. Miranda evaluates the arguments only when
the value is required. This evaluation scheme is called normal-order evaluation
or lazy evaluation.
Copy Mechanisms
formal parameter on entry but the value of the formal parameter is not copied
to the actual parameter on exit. In imperative languages, copying is
unnecessary if the language prohibits assignment to the formal parameter. In
such a case, the parameter may be passed by reference. This form of
parameter passing is often referred to as passing by value
When the passing by value and result are combined, the passing mechanism is
referred to as passing by value-result. Ada’s in out parameter is an
example of a parameter which may be passed by this form of the copy
mechanism. The value of the actual parameter is copied into the formal
parameter on entry and the value of the formal parameter is copied into the
actual parameter upon exit.
The copy mechanism has some disadvantages. The copying of large composite
values (arrays etc) is expensive and the parameters must be assignable (eg file
types in Pascal are not assignable).
Definitional Mechanisms
Name
procedure swap(x,y:sometype);
var x:sometype
begin
14.9. SAFETY 189
t := x; x := y; y := t
end;
...
I := 1
a[I] := 3
swap(I,a[I])
Parameter passing by name with assignment and when there are two
parameters one of which references the other.
aliasing
Unification
14.9 Safety
The purpose of declarations is two fold. The requirement that all names be
declared is essential to provide a check on spelling. It is not unusual for a
programmer to mispell a name. When declarations are not required, there is
no way to determine if a name is new or if it is a misspelling of a privious
name.
The second purpose of declarations is assist the type checking algorithm. The
type checker can determine if the intended type of a variable matches the use
of the variable. This sort of type checking can be performed at compile time
permitting the generation of more efficient code since run time type checks
need not be performed.
import/export
information not supplied by the programmer. This is error prone since slight
errors may radically affect what the compiler does.
14.11 Exercises
Chapter 15
Translation
• The lexical phase (scanner) groups characters into lexical units or tokens.
The input to the lexical phase is a character stream. The output is a
stream of tokens. Regular expressions are used to define the tokens
recognized by a scanner (or lexical analyzer). The scanner is
implemented as a finite state machine.
• The parser groups tokens into syntactical units. The output of the
parser is a parse tree representation of the program. Context-free
grammars are used to define the program structure recognized by a
parser. The parser is implemented as a push-down automata.
• The semantic analysis phase analyzes the parse tree for context-sensitive
information often called the static semantics. The output of the
191
192 CHAPTER 15. TRANSLATION
• The code generator transforms the simplified annotated parse tree into
object code using rules which denote the semantics of the source
language.
There are several other types of translators that are often used in conjunction
with a compiler to facilitate the execution of programs. An assembler is a
translator whose source language (an assembly language) represents a
one-to-one transliteration of the object machine code. Some compilers
generate assembly code which is then assembled into machine code by an
assembler. A loader is a translator whose source and object languages are
machine language. The source language programs contain tables of data
specifying points in the program which must be modified if the program is to
be executed. A link editor takes collections of executable programs and links
them together for actual execution. A preprocessor is a translator whose
source language is an extended form of some high-level language and whose
object language is the standard form of the high-level language.
15.1 Parsing
15.2 Scanning
Between the programmer’s view of the program and the virtual machine
provided by the operating system is another virtual machine. It consists of the
data structures and algorithms necessary to support the execution of the
program. This virtual machine is the run time system of the language. Its
complexity may range in size from virtually nothing, as in the case of
FORTRAN, to an extremely sophisticated system supporting memory
management and inter process communication as in the case of a concurrent
programming language like SR. The run time system for Simp as includes the
processing unit capable of executing the code and a data area in which the
values assigned to variables are accessed through an offset into the data area.
15.5 Optimization
It may be possible to restructure the parse tree to reduce its size or to present
a parse to the code generator from which the code generator is able to produce
more efficient code. Some optimizations that can be applied to the parse tree
194 CHAPTER 15. TRANSLATION
are illustrated using source code rather than the parse tree.
Constant folding:
I := 4 + J - 5; --> I := J - 1;
or
I := 3; J := I + 2; --> I := 3; J := 5
From:
while (count < limit) do
INPUT SALES;
VALUE := SALES * ( MARK_UP + TAX );
OUTPUT := VALUE;
COUNT := COUNT + 1;
end; -->
to:
TEMP := MARK_UP + TAX;
while (COUNT < LIMIT) do
INPUT SALES;
VALUE := SALES * TEMP;
OUTPUT := VALUE;
COUNT := COUNT + 1;
end;
From:
For I := 1 to 10 do
A[I] := A[I] + E
to:
For I := address of first element in A
to address of last element in A
increment by size of an element of A do
A[I] := A[I] + E
From:
A := 6 * (B+C);
D := 3 + 7 * (B+C);
E := A * (B+C);
to:
TEMP := B + C;
A := 6 * TEMP;
D := 3 * 7 * TEMP;
E := A * TEMP;
Strength reduction:
2*x --> x + x
2*x --> shift left x
Mathematical identities:
The data segment contains the values associated with the variables. Each
variable is assigned to a location which holds the associated value. Thus, part
of the activity of code generation is to associate an address with each variable.
The code segment consists of a sequence of operations. Program constants are
incorporated in the code segment since their values do not change. The
expression stack is a stack which is used to hold intermediate values in the
evaluation of expressions. The presence of the expression stack indicates that
the virtual machine for Simp is a “stack machine”.
As an example of code generation, we extend our Lex and Yacc files for Simp
to generate code for a stack machine. First, we must extend the Yacc and Lex
files to pass the values of constants from the scanner to the parser. The
definition of the semantic record in the Yacc file is modified that the constant
may be returned as part of the semantic record.
196 CHAPTER 15. TRANSLATION
Then the Lex file is extended to place the value of the constant into the
semantic record.
%{
#include <string.h> /* for strdup */
#include <stdlib.h> /* for atoi */
#include "simple.tab.h" /* for token definitions and yylval */
%}
DIGIT [0-9]
ID [a-z][a-z0-9]*
%%
{DIGIT}+ { yylval.intval = atoi( yytext );
return(INT); }
...
{ID} { yylval.id = (char *) strdup(yytext);
return(IDENT); }
[ \t\n]+ /* eat up whitespace */
. { return(yytext[0]);}
%%
The symbol table record is extended to contain the offset from the base
address of the data segment (the storage area which is to contain the values
associated with each variable) and the putsym function is extended to place
the offset into the record associated with the variable.
struct symrec
{
char *name; /* name of symbol */
int offset; /* data offset */
struct symrec *next; /* link field */
};
typedef struct symrec symrec;
symrec *sym_table = (symrec *)0; /* Ptr to symbol table */
symrec *st_entry; /* Ptr to an entry */
symrec *putsym ();
symrec *getsym ();
symrec *
putsym (sym_name)
15.6. CODE GENERATION 197
char *sym_name;
{
symrec *ptr;
ptr = (symrec *) malloc (sizeof(symrec));
ptr->name = (char *) malloc (strlen(sym_name)+1);
strcpy (ptr->name,sym_name);
ptr->offset = data_offset++;
ptr->next = (struct symrec *)sym_table;
sym_table = ptr;
return ptr;
}
...
%{
#include <stdio.h> /* For I/O */
#include <stdlib.h> /* for malloc here and in symbol table */
#include <string.h> /* for strcmp in symbol table */
int data_offset = 0; /* for data area address offsets */
int label_count = 0; /* for label identifiers */
#include "ST.h"
#define YYDEBUG 1
%}
%union semrec /* The Semantic Records */
{
int intval; /* Integer values */
char *id; /* Identifiers */
struct lbs /* Labels for if and while */
{
int label0;
int label1;
} *lbls;
}
%token <intval> INT /* Simple integer */
%token <id> IDENT /* Simple identifier */
%token <lbls> IF WHILE /* For back-patching labels */
198 CHAPTER 15. TRANSLATION
The semantic record is extended to hold two label identifiers since two labels
will be required for the if and while commands.
The remainder of the file contains the actions associated with code generation
for a stack-machine based architecture.
n := 1;
if n < 10 then x := 1; else skip; fi;
while n < 10 do x := 5; n := n+1; end;
skip;
Following code generation there are further optimizations that are possible.
The code is scanned a few instructions at a time (the peephole) looking for
combinations of instructions that may be replaced by more efficient
combinations. Typical optimizations performed by a peephole optimizer
include copy propagation across register loads and stores, strength reduction
in arithmetic operators and memory access, and branch chaining.
For information on compiler construction using Lex and Yacc see[27]. Pratt
[24] emphasizes virtual machines.
200 CHAPTER 15. TRANSLATION
load_int 1
assign 0
load_var 0
load_int 10
less_than
jmp_false label 0
load_int 1
assign 1
goto label 1
label 0
label 1
label 2
load_var 0
load_int 10
less_than
jmp_false label 3
load_int 5
assign 1
load_var 0
load_int 1
add
assign 0
goto label 2
label 3
Evaluation of Programming
Languages
The first requirement for a general purpose programming languge is that its
computational model must be universal. That is, every problem that has an
algorithmic solution must be solvable in the computational model. This
requirement is easily met as the lambda calculus and the imperative model
show.
Functional Programming:
Logic Programming:
Imperative Programming:
Object-Oriented Programming:
Concurrent Programming:
16.2 Syntax
201
202 CHAPTER 16. EVALUATION OF PROGRAMMING LANGUAGES
16.3 Semantics
16.4 Pragmatics
• Naturalness for the application (relations, functions, objects, processes)
16.4. PRAGMATICS 203
Applicability
Safety
Abstraction
Generalization
Implementation
History
LISP (LISt Processing) was designed by John McCarthy in 1958. LISP grew
out of interest in symbolic computation. In particular, interest in areas such as
mechanizing theorem proving, modeling human intelligence, and natural
language processing. In each of these areas, list processing was seen as a
fundamental requirement. LISP was developed as a system for list processing
based on recursive functions. It provided for recursion, first-class functions,
and garbage collection. All new concepts at the time. LISP was inadvertantly
implemented with dynamic rather than static scope rules. Scheme is a modern
incarnation of LISP. It is a relatively small language with static rather than
dynamic scope rules. LISP was adopted as the language of choice for artificial
intelligence applications and continues to be in wide use in the aritficial
intelligence community.
ML
Miranda
Haskell is a modern language named after the logician Haskell B. Curry, and
designed by a 15-member international committee. The design goals for
Haskell are have a functional language which incorporates all recent “good
ideas” in functional language research and which is suitable for for teaching,
research and application. Haskell contains an overloading facility which is
incorporated with the polymorphic type system, purely functional i/o, arrays,
data abstraction, and information hiding.
207
208 CHAPTER 17. HISTORY
Imperative languages have a rich and varied history. The first imperative
programming languages were machine instructions. Machine instructions were
soon replaced with Assembly languages, essentially transliterations of machine
code.
FORTRAN (FORmula TRANslation) was the first high level language to gain
wide acceptance. It was designed for scientific applications and featured an
algebraic notation, types subprograms and formatted input/output. It was
implemented in 1956 by John Backus at IBM specifically for the IBM 704
machine. Efficient execution was a major concern consequently, its structure
and commands have much in common with assembly languages. FORTRAN
won wide acceptance and continues to be in wide use in the scientific
computing community.
Simula 67
Modula-2
Logic
A.1.1 Syntax
The compound formulas of sentential logic (with the exception of the negation
of an atomic formula) are classified as of type α with subformulas α1 and α2
α α1 α2
¬¬A A A
A∧B A B
¬(A ∨ B) ¬A ¬B
¬(A → B) A ¬B
A↔B A→B B→A
211
212 APPENDIX A. LOGIC
β β1 β2
A∨B A B
¬(A ∧ B) ¬A ¬B
A→B ¬A B
¬(A ↔ B) ¬(A → B) ¬(B → A)
The formulas of sentential logic are often called: sentential formulas, sentential
expressions, propositional formulas, propositional expressions, or simply
sentence or proposition.
A.1.2 Semantics
The semantics of sentential logic are the rules for classifying a sentence as true
or false. The semantic rules that we give here are analytic rules because the
truth of a compound formula is determined by the truth of its subformulas. A
type α formula is classified as true iff both of its subformulas are true. A type
β formula classified as false iff one of its subformulas is true. Here are some
essential definitions.
Definition A.6 A sentence that can be true for some classification of its
subformulas is called satisfiable.
C S, α
A:
C S, α1 , α2
C S,β
B:
C S,β1 , S,β2
where C is the rest of the configuration, S is the set of the rest of the
elements of the block, and α and β indicate the type of the compound formula.
These definitions and the α and β rules form the base for the method of proof
using analytic tableaux. The method involves searching for contradictions
among the formulas generated by application of the analytic properties.
B1 ,..., Bn
C Bi
C Bi0
The rule is read as “replace block Bi in the configuration with block Bi0 .” The
configuration reduction rules are based on the analytic properties and are
found in Table A.1. Each reduction rule corresponds to one of the analytic
214 APPENDIX A. LOGIC
¬[(p ∨ q) → (p ∧ q)]
(p ∨ q), ¬(p ∧ q)
p, ¬(p ∧ q) , q, ¬(p ∧ q)
p, ¬p , p, ¬q , q, ¬p , q, ¬q
Note that the outer two blocks of the last line are contradictory and the inner
two define an interpretation under which the formula is satisfiable.
A.2.1 Syntax
1. A sentence is also a predicate with no free variables.
2. If p is a predicate and x is a variable but not a variable in p and T is a
type then the following are also predicates. ∀x.T : p(x) and ∃x.T : p(x)
which have the same free variables as p but have in addition the bound
variable x. p(x) is formed from p by replacing any number of occurrences
of some constant of type T in p with x. x is said to be free in p(x) and is
said to be bound in ∀x.T : p(x) and ∃x.T : p(x).
The additional formulas of Predicate Logic are compound formulas and the
universally quantified formulas are of type γ with subformula γ(c). A type γ
formula holds iff its subformula holds for each constant of the appropriate type.
A.2. PREDICATE LOGIC 215
C S,γ
C: some constant c
C S,γ(c), γ
C S,δ
D: a constant c new to C S,δ
C S,δ(c)
where C is the rest of the configuration, S is the set of the rest of the
elements of the block, and γ and δ are the types of compound formulas.
γ γ(c)
∀x.T : P (x) P (c)
¬∃x.T : P (x) ¬P (c)
δ δ(c)
∃x.T : P (x) P (c)
¬∀x.T : P (x) ¬P (c)
A.2.2 Semantics
A type γ formula holds iff its subformula holds for each constant in the
universe of discourse. A type δ formula holds iff its subformula holds for some
constant in the universe of discourse. Here are some essential definitions. The
configuration reduction rules for these formulas are based on the analytic
properties and are found in Table A.2.
216 APPENDIX A. LOGIC
Bibliography
[1] Abelson, H., Sussman, G.J., and Sussman, J. Structure and Interpretation
of Computer Programs. MIT Press, Cambridge, Massachusetts, 1985.
[2] Backus, J. W., “Can Programming Be Liberated from the von Neumann
Style?” CACM, vol. 21, no. 8, pp. 613-614.
[3] Barendregt, H. P., The Lambda Calculus: Its Syntax and Semantics. 2d
ed. North-Holland, 1984.
[5] Boehm, C. and Jacopini, G., “Flow Diagrams, Turnign Machines, and
Languages with Only Two Foramation Rules.” CACM, vol. 9, no. 5, pp.
366-371.
[7] Curry. H. B., Hindley, J. R., and Seldin, J. P., Combinatory Logic, Vol. II.
North-Holland, 1972.
[8] Deransart, P., Jourdan, M., and Lorho, B., Attribute Grammars:
Definitions, Systems and Bibliography. Lecture Notes in Computer
Science 323. Springer-Verlag, 1988.
[10] Gries, D., The Science of Programming Springer-Verlag, New York, 1981.
217
218 BIBLIOGRAPHY
[19] McCarthy, J., Abrahams, P. W., Edwards, D. J., Hart, T. P., and Levin,
M., LISP 1.5 Programmer’s Manual. 2d ed. MIT Press, Cambridge, MA.
1965.
[21] Miller, G. A., The Psychology of Communication. Basic Books, New York,
1967.
[23] Pittman, T. and Peters, J., The Art of Compiler Design: Theory and
Practice. Prentice-Hall, 1992.
[29] Steele, G. L., Jr., Common Lisp. Digital Press, Burlington, MA. 1984.
[30] Tennent, R. D., Principles of Programming Languages, Prentice-Hall
International, 1981.
[31] Wegner, Peter, “Concepts and Paradigms of Object-Oriented
Programming.” OOPS Messenger vol. 1 no. 1 (August 1990): pp. 7-87.
[32] Worf, Benjamin, Language thought and reality, MIT Press, Cambridge
Mass., 1956.