Organisation of Programming Languages
Organisation of Programming Languages
Organization of
Programming Languages
© 1991 by Springer-VerlaglWien
With 50 Figures
Beside the computers itself, programming languages are the most important tools
of a computer scientist, because they allow the formulation of algorithms in a way
that a computer can perform the desired actions. Without the availability of (high
level) languages it would simply be impossible to solve complex problems by using
computers. Therefore, high level programming languages form a central topic in
Computer Science. It should be a must for every student of Computer Science to
take a course on the organization and structure of programming languages, since
the knowledge about the design of the various programming languages as well as
the understanding of certain compilation techniques can support the decision to
choose the right language for a particular problem or application.
This book is about high level programming languages. It deals with all the major
aspects of programming languages (including a lot of examples and exercises).
Therefore, the book does not give an detailed introduction to a certain program-
ming language (for this it is referred to the original language reports), but it explains
the most important features of certain programming languages using those pro-
gramming languages to exemplify the problems. The book was outlined for a one
session course on programming languages. It can be used both as a teacher's ref-
erence as well as a student text book.
Chapter Two is about language processing. Syntax and semantics, formal lan-
guages and grammars are introduced as well as compiler aspects and run-time
environments. These are the fundamental aspects which are important for the un-
derstanding of certain problems of programming languages.
vi
Chapter Three deals with data types. The concept of binding is discussed followed
by an introductory to elementary data types, structured data types, as well as ab-
stract data types. Problems of type checking and the implementation of data types
are outlined. Finally variables - the carriers of certain types - are considered in
terms of scope and lifetime.
Chapter Six deals with data encapsulation. The basic ideas of data encapsulation
(and, thus, of abstract data types) are introduced. Concepts of abstraction, informa-
tion hiding, and encapsulation are considered before certain abstraction tech-
niques in SIMULA 67, C++, EIFFEL, MODULA-2, and ADA are discussed.
Chapter Nine introduces briefly the three major approaches in the description of
semantics in programming languages. these approaches are: Operational seman-
tics, denotational semantics, and axiomatic semantics. The first method introduces
vii
a virtual computer to describe the meaning of basic language concepts, while the
others represent a mathematical way of description.
Acknowledgements
Bernd Teufel
Contents
Preface.............................................................................................................................v
Contents ..........................................................................................................................ix
5 Procedures ..............................................................................................................88
5.1 Basic Ideas ...................................................................................................... 88
7 Inheritance............................................................................................................... 1 36
7.1 Basic Ideas ...................................................................................................... 136
7.2 Sub ranges and Subtypes ............................................................................. 138
8 Concurrency............................................................................................................ 154
8.1 Basic Ideas ...................................................................................................... 154
8.2 Coroutines in SIMULA 67 ............................................................................ 157
8.3 Semaphores .................................................................................................... 160
8.4 Monitors............................................................................................................ 163
Index .................................................................................................................................203
1 Principles of Programming Languages
1
FORTRAN
/I
LISP
COBOL
!~p(' B:SIC
SCHEME ALGOL 60
/+~
ALGOLS8
AlGI-W PU1
PROLOG
PASCAL
C
PASCAL-SC MODULA-2
1
OBERON
EIFFEL
Computers usually operate on a binary level, i.e. computers understand just se-
quences of D's and 1'so Thus, in the early days of computer programming binary
machine code was used to program computers. We call such codes first generation
languages, even though there are not less people refusing the term languages for
binary machine code (Sammet, for example, even does not consider assembly
languages to be programming languages, cf [SAMM 69]).
Binary programming means that the program directly reflects the hardware struc-
ture of the computer. Sequences of D's and 1's cause certain actions in processing,
control, or memory units, i.e. programming was done on the flip-flop level. It was
recognized very rapidly that programming computers on binary machine code level
is very troublesome. For example, the insertion of a new instruction - a normal
situation in program development - results in incorrect addresses in the already
1.1 Evolution of Programming Languages 3
existing instructions. Thus, it is obvious that binary programming could not success-
fully be applied to solve complex problems. A small step forward was done by the
introduction of programming in octal or hexadecimal code which caused also the
development of the first "compilation systems." Obviously, this was not enough.
Assembly languages still reflect the hardware structure of the target machine - not
on the flip-flop level, but on the register level, i.e. the abstraction has changed from
the flip-flop to the register level. The instruction set of the target computer directly
determines the scope of an assembly language.
When considering high level programming languages, there are four groups of
programming languages (and, therefore, programming styles) distinguishable:
Often programming languages cannot be clearly associated with one of these four
groups. For example, languages like MODULA-2 or OBERON are sometimes said
to be imperative although they contain object-oriented features. In the following a
brief description of the historical highlights is given.
In the 1950s and early 1960s fundamental concepts on compilers have been de-
veloped and the first compilers have been introduced (for example the first
FORTRAN compiler, cf [BACK 57]), a precondition for the design of high level pro-
gramming languages. Now, the first step was done to produce more reliable pro-
grams, since structured programming became possible, and to get portability on
source code level.
• FORTRAN,
1.1 Evolution of Programming Languages 5
• COBOL.
string processing. Thus, its emphasis is on character string data and the appropri-
ate pattern matching features.
Back to imperative programming languages. In the mid 1960s IBM launched a very
aspiring project: PU1 (but its first standard definition report was published more
than 10 years later PU1 76]). This new language PU1 (Programming Language 1)
was thought to become a general purpose programming language containing ele-
ments from ALGOL 60, FORTRAN, COBOL, and LISP. Exception handling and
multi-tasking were the new features introduced by PU1. The language was the
most complex language of its days and its success is questionable - similar to
those other complex languages like ALGOL 68 and ADA. Dijkstra once said about
PU1 in his ACM Turing Award Lecture [DIJK 72]: "Using PU1 must be like flying a
plane with 7000 buttons. switches, and handles to manipulate in the cockpit."
SIMULA 67 was another important language which was designed during the
1960s. Nygaard and Dahl designed SIMULA 1 in the early 1960s at the Norwegian
Computing Center and thereafter made further developments which resulted in
SIMULA 67 [DAHL 66]. SIMULA was not just another kind of a general purpose
language like PU1, rather it was designed to handle simulation problems. The lan-
guage is in many of its constructs a direct successor of ALGOL 60. The new feature
which was introduced by SIMULA 67 was the class concept: a mechanism to group
together data structures and procedures to manipulate the data structures. This
concept can be seen as the introduction of abstract data types. Hierarchies of
classes can be defined, and the inheritance concept is applied. The definition of a
class is distinct from its instance, i.e. a program can dynamically generate several
instances of the same class.
The next decade, the 1970s, started with the introduction of PASCAL [WIRT 71a],
[JENS 74]. Although Wirth (now back in Switzerland and a member of the former
Fachgruppe Computer-Wissenschaften at the Eidgenossische Technische Hoch-
schule, ZOrich) primarily designed PASCAL as a language for teaching, its practi-
cal usability was also a design goal: "In fact, I do not believe in using tools and for-
malisms in teaching that are inadequate for any practical tasK', [WIRT 85]. With
PASCAL an elegant programming language was designed from which an efficient
code can be generated. PASCAL is mainly based on the ideas of ALGOL 60, but it
contains also elements from ALGOL-W (while and case statement) and ALGOL 68
(user defined data types). The language supports strongly the efforts on structural
programming and is very attractive because of its possibilities to structure data,
especially the possibility of user defined data types. PASCAL gained in signifi-
cance in the late 1970s, when more and more personal computers and microcom-
puters came in use.
It took not a long time that Wirth started to work on new ideas: To work out rules for
multiprogramming and to find mechanisms to support modularity in program sys-
tems. The result was MODULA and later MODULA-2, introduced in the late 1970s
[WIRT 78], [WIRT 88a]. MODULA-2 includes all aspects of PASCAL and extends
them with the important module concept. Thus, modules are the major issues of
MODULA-2: program units that can include type definitions, objects, and proce-
dures which can be accessed by other program units. But the sequence of lan-
guages that were designed by Niklaus Wirth does not end with MODULA-2. The
latest outcome from his research work is called OBERON [WIRT88b], [WIRT 88c],
[WIRT 90). OBERON is based on MODULA-2, it has a few additions (e.g. the facility
of extended record types) and several subtractions (e.g. local moduls were elimi-
nated in OBERON, since the experience with MODULA-2 has shown that they are
rarely used, [WIRT 88b)). Wirth's motto for the design of OBERON was Einstein's
word: "Make it as simple as possible, but not simplet' [WIRT 88c].
Back to the 1970s. There was an imperative language, a logical language, and an
object-oriented language introduced which have to be mentioned here: C, PRO-
LOG, and SMALLTALK. C [KERN 78], [KERN 88] is a successor of CPL (Combined
Programming Language), and BCPL (Basic CPL), [RICH 69]. Kernighan and
Ritchie introduce C in their report in the following way, [KERN 88]: "C is a general
purpose programming language which features economy of expression, modern
control flow and data structures, and a rich set of operators. C is not a 'very high
level' language, nor a 'big' one, and is not specialized to any particular area of
application." Since C was originally designed for the implementation of the UNIX
operating system, it is closely related with UNIX and makes programming in a UNIX
environment easy. Although there were (and still are) considerable critical voices
about C, the popularity of C grew with the popularity of UNIX.
8 1 Principles of Programming Languages
SMALLTALK is a result of the ideas which Alan Kay at the University of Utah had
already in the late '60s. He saw the potential of personal computers and the neces-
sity of a programming environment for non-programmers. In the early 1970s Kay
designed at Xerox PARC the language FLEX, which is a predecessor of SMALL-
TALK. Data and procedures manipulating the data are objects, and SMALLTALK is
probably the most prominent representative of object-oriented languages. The
communication between objects is done by messages. Objects can return other
objects in reply to a message [GOLD 83].
But there is one more language which cannot be forgotten when talking about the
evolution of programming languages: ADA [ADA 79], [BYNE 91]. The design of
ADA was a very ambitious and expensive project launched by the US Department
of Defense (DoD). ADA was thought to become a general purpose language for
large software projects providing all the state-of-the-art concepts of conventional
programming languages and, therefore, being an instrument against what is called
software crisis. The major concepts of ADA are: Modularity, data abstraction, sepa-
rate compilation, exception handling, concurrency, and generic procedures. There
exist very controversial opinions about ADA and its usability. The most critical
points of ADA are its size and complexity. ADA's objectives were readability and
simplicity to make reliable programs possible and maintenance easy. But exactly
these objectives were not achieved by the design of ADA, what rises the question,
whether ADA programs are reliable and secure. Hoare, one of the advisers of the
project, is therefore very critical about the usage of ADA and he appealed to the
representatives of the programming profession in the US and to all concerned with
the welfare and safety of mankind [HOAR 81]: "00 not allow this language in its
present state to be used in applications where reliability is critical, i.e. nuclear
power stations, cruise missiles, early warning systems ... An unreliable program-
ming language generating unreliable programs constitutes a far greater risk to our
environment and to our society than unsafe cars, toxic pesticides, or accidents at
nuclear power stations. Be vigilant to reduce that risk, not to increase it." Here, the
sense of responsibility of all computer scientists is required.
1.1 Evolution of Programming Languages 9
In the last four decades much more languages and dialects of these languages
have been designed. Lets consider PASCAL, for example, there exist several spe-
cialized extensions: CONCURRENT PASCAL [BRIN 75a] , UCSD PASCAL [BOWL
79], PASCAL PLUS [BUST 78], PASCAUR [SCHM 80], or PASCAL-SC [BOHL 81],
to name only a few of these extensions, subsets, or dialects. But its beyond the
scope of this text to give a complete historical overview of programming languages.
The selection was based on the importance of the languages.
Fourth generation languages deal with the following two fields which become more
and more important:
The steadily increasing usage of software packages like database systems, spread
sheets, statistical packages, and other (special purpose) packages makes it neces-
sary to have a medium of control available which can easily be used by non-spe-
cialists. In fourth generation languages the user describes what he wants to be
solved, instead of how he wants to solve a problem - as it is done using procedu-
ral languages. In general, fourth generation languages are not only languages, but
interactive programming environments.
One of the best known database query languages is probably SOL (Structured
Query Language): a query language for relational databases which was developed
at IBM and which is based on Codd's requirements for non-procedural query lan-
guages for relational databases. NATURAL is another approach in this field em-
phazising on a structured programming style. Program or application generators
are often based on a certain specification method and produce an output (e.g. a
high level program) to an appropriate specification. There exist already a great
number of fourth generation languages:
• ADF,
• IDEAL,
• NATURAL,
• NOMAD,
• MANTIS,
• MAPPER, or
• RAMIS
10 1 Principles of Programming Languages
to name only a few. An overview of such languages is given in [MART 86]. A per-
formance analysis of several fourth generation languages and their comparison
with third generation COBOL programs is given in [MATO 89].
In this Section we want to give a brief overview of the views of Backus, Hoare, and
Wirth, three Computer Scientists whose work is closely related with the definition,
design, and development of programming languages. All of them are recipients of
the ACM Turing Award.
John Backus received the Award in 1977 for his "profound, influential, and lasting
contributions to the design of practical high-level programming systems, notably
through his work on FORTRAN, and for seminal publication of formal procedures
for the specification of programming languages [ACM 78]."
Charles Antony Richard Hoare received the Award in 1980 for his "fundamental
contributions to the definition and design of programming languages . ... He is best
known for his work on axiomatic definitions of programming languages through the
use of techniques popularly referred to as axiomatic semantics. He ... was respon-
sible for inventing and promulgating advanced data structuring techniques in sci-
entific programming languages [ACM 81]."
Niklaus Wirth was presented the 1984 Turing Award "in recognition of his outstand-
ing work in developing a sequence of innovative computer languages: EULER,
ALGOL-W, MODULA, and PASCAL. ... The hallmarks of a Wirth language are its
simplicity, economy of design, and high-quality engineering, which result in a lan-
guage whose notation appears to be a natural extension of algorithmic thinking
rather than an extraneous formalism [ACM 85]."
In the following we quote from their Turing Award Lectures [BACK 78], [HOAR 81],
and [WIRT 85].
John Backus
John Backus describes basic defects in the framework of conventional languages
and suggests them to be responsible for the expressive weakness and cancerous
growth of such languages. He suggests applicative or functional languages as an
alternative:
parent - the von Neumann computer - must be examined. Von Neumann comput-
ers are build around a bottle-neck: the word-at-a-time tube connecting the CPU
and the store. Conventional languages are basically high level, complex versions
of the von Neumann computer. Thus variable = storage cells; assignment state-
ments = fetching, storing, and arithmetic; control statements = jump and test in-
structions. The symbol ":=" is the linguistic von Neumann bottle-neck. Von
Neumann languages split programming into a world of expressions and a world of
statements; the first of these is an orderly world, the second is a disorderly one, a
world that structured programming has simplified somewhat, but without attacking
the basic problems of the split itself and of the word-at-a-time style of conventional
languages.
When comparing a von Neumann program and a functional program for inner
product, a number of problems of the former and advantages of the latter can be il-
lustrated: e.g., the von Neumann program is repetitive and word-at-a-time, works
only for two vectors with given names and length, and can only be made general
by use of a procedure declaration, which has complex semantics. The functional
program is non-repetitive, deals with vectors as units, is more hierarchically con-
structed, is completely general, and creates "housekeeping" operations by com-
posing high-level housekeeping operators. It does not name its arguments, hence
it requires no procedure declaration. The defects of conventional languages cannot
be resolved unless a new kind of language framework is discovered.
Backus studied the area of non-von Neumann systems very carefully and his
search indicates a useful approach to designing non-von Neumann languages.
This approach involves four elements:
In the early 1960s, when designing a modest subset of ALGOL 60 for the imple-
mentation on the then next computer generation, Hoare adopted certain basic
principles which he believes to be as valid today as they were then:
ii) Brevity of the object code produced by the compiler and compactness of
run time working data. There is a clear reason for this: The size of main
storage on any computer is limited and its extension involves delay and
expense.
iii) Entry and exit conventions for procedures and functions should be as
compact and efficient as for tightly coded machine code subroutines.
Since procedures are one of the most powerful features of high level
languages, there must be no impediment to their frequent use.
iv) The compiler should use only a single pass. It has to be structured as a
collection of mutually recursive procedures, each capable of analyzing
and translating a major syntactic unit of the language.
Hoare advocates these principles also when being a member of the ALGOL
committee or when being an adviser to the 000 ADA project. But in both cases his
advices died away unheard. His warnings and suggestions:
Niklaus Wirth
Nicklaus Wirth recognizes that the complex world around us often requires com-
plex mechanisms. However, this should not diminish our desire for elegant solu-
1.2 Backus, Hoare, and Wirth 13
tions, which convince by their clarity and effectiveness. Simple, elegant solutions
are more effective, but they are harder to find than complex ones. His principal aim
was and still is simplicity and modularity in program systems.
The size of the ALGOL-W compiler grew beyond the limits within which one could
rest comfortably with the feeling of having a grasp, a mental understanding, of the
whole program. Systems programming requires an efficient compiler generating
efficient code that operated without a fixed, hidden, and large so-called run-time
package. This goal had been missed by both ALGOL-Wand PU1, both because
the languages were complex and the target computers inadequate. Wirth over-
came these drawbacks with PASCAL and most of all with the combination of
MODULA-2 and the Lilith-workstation.
The module is the key to bringing under one hat the contradictory requirements of
high level abstraction for security through redundancy checking and low level fa-
cilities that allow access to individual features of a particular computer. It lets the
programmer encapsulated the use of low level facilities in a few small parts of the
system, thus protecting him from falling into their traps in unexpected places.
Wirth distilled a few characteristics which were common to all of his projects:
Every single project was primarily a learning project. One learns best
when inventing, and teaching by setting a good example is often the
most effective method and sometimes the only one available.
Resume
While Backus sees the cause of most problems of todays programming systems in
the von Neumann bottle-neck and the great influence of the von Neumann com-
puter architecture to the design of programming languages, both Hoare and Wirth
see the evil in the cruse of complexity. Backus suggests a functional style of pro-
gramming and, thus, functional programming languages influencing the hardware
design of new computers (and not vice versa).
Hoare and Wirth are advocates of simplicity and modularity, since simple and ele-
gant solutions are prerequisites for reliable - and therefore successful - systems.
The disaster with all these well-known highly complex programming languages
proofs their views. We think that just changing from imperative to functional, logical,
or object-oriented languages and programming styles does not help much; we
have also to consider the principle of simplicity - no matter what kind of language
we use.
Data abstraction.
Control abstraction.
Crystallisation Process
The next step in data abstraction is given by the possibility of user-defined data
types. But this is only one half of the abstraction, since the manipulation of vari-
ables of those user-defined data types is not clearly given in form of operations
which can be applied to those data types. Afford reliefs the introduction of abstract
data types:
An abstract data type is a (user-defined) data type and the set of permit-
ted operations on objects of this type, by which the internal representa-
tion is hidden to those using that type.
Control abstraction is the mechanism defining the order in which certain activities
or groups of activities should be performed. By this we mean not only control
16 1 Principles of Programming Languages
structures on the statement level like conditional and unconditional branching, but
also the construction of subprograms up to concurrent programs. Control abstrac-
tion on the subprogram level was already introduced in assembler languages by
allowing macro definitions.
But before starting with meta-languages for the description of programming lan-
guages, we should define some of the most important elements of languages.
Definitions
empty string E. The empty string e is that string which contains no sym-
bols. The sequence 0011 is an example of a string over the alphabet A1.
Production: Rules for string substitution are called productions. The sym-
bols ~ and ::= are widely used to represent productions. For example,
the rule (production)
s~ab (or s ::= a b )
means that s can be substituted by a b, or s is defined as a b.
The * in these definitions indicates the closure of a certain set. We will find a similar
usage of this operator for productions.
BNF
Backus-Naur form (BNF) was first introduced for the definition of the syntactical
structure of the programming language ALGOL 60 (cf. [NAUR 63]). It is the most
popular form for the precise syntactical definition of programming languages.
symbol meaning
-t "is defined as"
end of a definition
1 "or", alternative
[x 1 one or no occurrence of x
{ x} an arbitrary occurrence of x (0,1,2, ... )
( xl y ) selection (x or y )
T { +, -, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
int [ + 1- 1 unsigned_int.
unsigned_int digit 1 unsigned_int digit.
digit 0111213141516171819.
The first rule defines that an integer is an unsigned integer with a leading sign. This
sign can be absent, or "+", or "_". The second rule shows that BNF allows recursive
definitions.
Syntax Diagrams
One way to represent the syntactical structure of a language is to use the BNF-no-
tation. Syntax diagrams or graphs are another - a graphical - way to represent
the syntax of a language. The graphical syntax representation makes a language
definition easy to survey.
1.4 Basic Terminology and Language Description 19
N a1 I a2 I ... I an
will be represented by the following graph:
I a1 I
l J
I a2 I
I I
... ,Ir
· .~
·
·
I an I
I I
R2. Terms of the form
[a] ,
{ a} ,
20 1 Principles of Programming Languages
Obviously, a given sentence will be correct, if and only if the elements of the sen-
tence describe a correct path through the graphs. Using the rules R1 - R6 we can
transform the BNF-notation for integers as given above into the following syntax
graphs.
int
digit
1.4 Basic Terminology and Language Description 21
digit
2 Language Processing
k integer;
we can explain how integer variables are defined in PASCAL. The semantics of
this statement tells us that space for an integer variable is reserved which - if the
statement is contained in the declaration part of a procedure - can only be ac-
cessed during the existence of that block in which it is declared. Then, considering
2.1 Syntax vs Semantics 23
k := k + 1;
purely from a syntactical viewpoint, "k", ":=", "+", "1", and ";" are just symbols; "1"
has nothing to do with the mathematical object 1. A semantic description of
PASCAL (say) might relate "k . = k + 1;" to the idea of incrementing the contents
of some memory cell.
While we already know some good formalisms - Backus-Naur form and syntax di-
agrams - by which the syntax of nearly all new programming languages is de-
scribed, as yet there is no equivalent semantic formalism available which achieved
the popularity of BNF or syntax diagrams. Thus, the semantics of programming lan-
guages are still often described in natural language which usually is not concise
enough, as we know. Thus, problems arise not only for programmers using such a
language, but most of all for the compiler writer who has to implement the lan-
guage. Anyhow, we want to introduce some methods for formal description of se-
mantics in Chapter 9.
In this section we give a brief introduction to formal languages. The field of formal
languages is a wide area and an independent field of research. A fundamental in-
troduction to this topic will be beyond the scope of this text. For a more detailed de-
scription on the theory of formal languages we therefore refer to the books [KUIC
86], [SALO 73], for example.
Thus, a grammar consists of a set of rules, where each nonterminal symbol is de-
fined. One of the nonterminal symbols is marked as a start symbol of the grammar.
For example, such a start symbol could be "PROGRAM" or "MODULE" when con-
sidering the languages PASCAL or MODULA-2, respectively.
We say that a string an can be derived from a string aO, if and only if there exists a
sequence of strings aO, a1, a2, ... , an-1 so that each ai can directly be derived by
ai-1 (i = 1, 2, ... , n):
Now, we can define a language L (G) as the set of all strings of terminal symbols
which can be derived from the start symbol S :
L = { (J I S ~* (J and (J E T*} .
Considering the derivation process we can find two important strategies: leftmost
derivations and rightmost derivations. A derivation is called leftmost (rightmost) if
always the leftmost (rightmost) nonterminal is replaced.
2.2 Fonnal Languages and Grammars 25
A BNF-rule
A context-free grammar will be called unambiguous, if and only if there exists just
one rightmost (leftmost) derivation and therefore one parse tree for each sentence
which can be derived by the productions of the grammar. Otherwise it will be called
ambiguous.
A sentence of an ambiguous grammar can have more than one parse tree and,
therefore, it can have more than one meaning. Thus, ambiguous grammars are not
very useful for the analysis and the definition of programming languages. They are
hard to handle and we normally try to transform them into unambiguous ones. It
should be noted, that it is an undecidable problem to determine whether a given
grammar is ambiguous or not. As we will see later, there exist some conditions,
which - if fulfilled - are sufficient to say that a certain grammar is unambiguous.
But these conditions are not necessary, that means that a grammar which does not
fulfil these conditions cannot be said to be ambiguous (or even unambiguous).
Hierarchy of Grammars
Type 0 grammars are no-restriction grammars, i.e. there are no restrictions neither
for the left side, nor for the right side of the productions. Such general grammars
are of no relevance for today's programming languages. Writing a parser for a type
o grammar would be a very hard task. The form of the productions of type 1 gram-
mars implies that replacements can only be done within a certain context, i.e. these
are context-sensitive grammars. In contrast to that, type 2 grammars are context-
free grammars, while the left- and right-linear type 3 grammars are regular gram-
mars. Clearly, a grammar of type i + 1 is also of type i, i = 0, 1, 2.
26 2 Language Processing
type 0: no restrictions
.
(left-linear)
A,B E N, a E T
Parse Trees
---------- ----------
EXPR
EXPR TERM
EXPR
/~TERM
/~
TERM FACTOR
TERM
I I
FACTOR
I
FACTOR
I
FACTOR
I
x + y x y
TO { x. y. +. -. *. I. (. )}
NO {EXPR. TERM. FACTOR}
Po { EXPR -t TERM I EXPR + TERM I EXPR - TERM
TERM -t FACTOR I TERM * FACTOR I TERM I FACTOR
FACTOR -t x I y I (EXPR) }
So {EXPR}
We see that each expression is a sequence of terms which are separated with" +"
or "-", Figure 2.2 shows the parse tree for the expression x + y - x * y , It represents
graphically the derivation of a sentence of the language according to the grammar
GO (TO. NO. PO. SO),
28 2 Language Processing
The process of generating a parse tree for a given expression will be called syntax
analysis or parsing.
Compiling a program means analysis and synthesis of that program, i.e. determin-
ing the structure and meaning of a source code and translating that source code
into an equivalent machine code. The major tasks or phases of a compiler are lexi-
cal analysis, syntax analysis, semantic analysis, and code generation. (cf Fig. 2.3).
Source
Syntactical Analysis
Object Code
Lexical Analysis
At the beginning of the compilation process the source code of a program is noth-
ing else. but a stream of characters. Thus. the task of the lexical analysis is to rec-
ognize symbols (which can be defined by regular grammars) in this stream of char-
acters and to provide these symbols in a more useful representation to the syntax
analysis.
a. b•...• Z
0.1 ..... 9
0.1 ..... 9
other
<
other
>
other
Fig. 2.4. Rudimentary transition diagram for a scanner for an imperative language
30 2 Language Processing
Syntax Analysis
The symbol sequences which are provided by the scanner are sequentially
analysed by a syntax analyser or parser. Thus, the parser decides whether a cer-
tain sequence of symbols will be accepted by the considered programming
language.
There are two main methods to do the syntactical analysis: top-down parsing and
bottom-up parsing. The most efficient top-down and bottom-up parsers are based
on so-called LL- and LR-grammars, respectively.
Top-down Analysis
To fulfil this rule we need some additional information, i.e. we need to know
2.3 Compiler Aspects 31
1) the set of all terminal symbols that may occur at the beginning of a sen-
tence that can be derived from an arbitrary sequence of symbols;
2) the set of all terminal symbols that may follow after a nonterminal.
where TE = T u { E }.
Let X be a nonterminal symbol. Then, FOLLOW(X) is the set of all terminal symbols
that can occur immediately to the right of X:
C2) If the empty string E can be derived from a nonterminal X, then it is required
that
FIRST(X) n FOLLOW(X) = 0.
The first "L" of LL(1) means that the input will be read from left to right, while the
second "L" indicates leftmost derivations. The "1" means that we look ahead one
symbol at any step of the parse process.
Characteristic C1 means that for a given input string there exists just one possible
production at a certain state of the derivation, i.e. it will be obvious which alternative
of a production should be applied. Characteristic C2 helps to avoid so-called dead
ends occurring by (BNF-) productions of the form
A { x} or A [x 1
where arbitrary occurrences (inclusive 0 times) are allowed (cf [TEUF 89]).
32 2 Language Processing
LL(1 )-grammars are preferably used for top-down parsing. They allow an analysis
with no dead ends, Le. with no wrong decisions. An overview of general top-down
parsing - which is often referred to as recursive descent parsing (or predictive
parsing) - is given in the following.
This general method of recursive descent parsing can be applied not only to LL(1)-
grammars, but only LL(1 )-grammars will guarantee that no dead ends and there-
fore no backtracking will occur, this means that always a valid production for a
nonterminal leaf will be selected (for examples see [TEUF 89]).
PROGRAM Parser;
PROCEDURE Error ..
(. ) ;
PROCEDURE NO;
PROCEDURE Nl;
PROCEDURE Nm;
BEGIN
Get_Symbol;
NO;
END.
These procedures are embedded into a main program containing also an error-
procedure and a procedure which provides the next symbol (Get_Symbol). The
principle structure of a recursive descent parser for the above mentioned grammar
G is shown in Figure 2.5.
Bottom-up Analysis
We start with the fundamental definition of a handle. In general, we can say that a
string's substring is called a handle, if it can be reduced using the left side of an
appropriate production, provided that the reduction corresponds to a step in the
leftmost reduction of the string to the grammars start symbol. Thus, a handle can
informally said to be representing a particular reduction step (or derivation step,
depending on the point of view). Clearly, handles (and therefore those reduction
steps) can be recognized by the parsing technique which will be introduced later in
this section.
S ~* aXt ~
a~t ~ aXt
The processing of a sentence using leftmost reductions can be handled very com-
fortably when using a stack mechanism. Then, the handle will be always on top of
the stack. Considering once again grammar GO (NO, TO, PO, SO), we are able to
exemplify this as follows (where the changes on top of stack are caused either by
pushing input symbols onto the stack or by reducing a handle on top of stack):
34 2 Language Processing
input stack
0 x+y -x
1 x + Y- x x
2 + Y- x FACTOR
3 + Y- x TERM
4 + Y- x EXPR
5 V-x EXPR+
6 -x EXPR +y
7 -x EXPR + FACTOR
8 -x EXPR + TERM
9 -x EXPR
10 x EXPR-
11 EXPR-x
12 EXPR - FACTOR
13 EXPR - TERM
14 EXPR
Here, the handle is bold faced. Reading the stack column from bottom to top we
can recognize the rightmost derivation of the given sentence:
The main actions using a stack are shifting and reducing, as shown in the above
given example. This means, that one symbol from the input buffer is shifted onto the
stack and if it is a handle it will be reduced to a nonterminal (i.e. the handle is re-
placed by the left side of an appropriate production).
S~*w
It can be shown that every LL(k)-grammar is also an LR(k)-grammar and that for
every LR(k)-grammar with k > 1 exists an equivalent LR(1 )-grammar (see for ex-
ample [WAIT 84] or [SALO 73]).
Now, bottom-up parsing means to generate a parse tree for a given input starting at
the leaves and working up to the root of the tree. This is equivalent to the leftmost
reduction (or the rightmost derivation) of a sentence (l E T* to the start symbol S of
the considered grammar.
REPEAT
IF Handle_on_Top_of_Stack THEN
Reduce (* replace top of stack by the left *)
(* side of a production *)
ELSE
Shift (* get next input symbol on stack *)
END;
UNTIL Input_Empty AND No_Handle_on_Top_of_Stack;
IFAxiom_on_Top_of_Stack THEN
Accept (* input was syntactically correct *)
ELSE
Reject (* input was syntactically incorrect *)
END;
We call this analysis shift-reduce analysis since the shift and reduce actions are
characteristically to this analysis.
In the above given introduction to bottom-up parsing it was not explained how we
decide whether to shift or to reduce in a specific situation (i.e. we gave no idea,
how a handle can be recognized). The information needed for this decision pro-
cess is being held in a so-called action-table and a goto-table. Then, the general
LR-parsing model is given in Figure 2.7. This model consists of an analysing pro-
gram, an input buffer, and the parse tables. The stack contains not only symbols of
the grammar, but also states indicating the contents of the stack.
The state on top of the stack together with the current input symbol determines the
above mentioned decision process, i.e. they index the parse table. Each state in
the stack reflects uniquely the proceeding analysis process.
a I + I b
Ia .I b $
Input
Stack
Sk
Xk 4
1
s k-1
X k- 1
push(current_input);
push(s); (* s is new top of stack *)
INC(read_pointer);
h := length(handle);
FOR i := 1 TO 2*h DO pop(_.);
(* pop h symbols of the grammar *)
(* and h states off the stack *)
s .- G[top_of_stack, left side(p)];
(* get the new top of stack *)
(* from the goto-table *)
push(left_side(p));
push (s) ;
Error ( ... ) ;
END;
END;
As we have already seen when discussing the general shift-reduce analysis, the
principle actions of the parser or analysing program are
shift, i.e. the next input symbol will be shifted onto the stack and depend-
ing on that symbol and the current stack a new state will get on top of
stack;
accept, Le. the input will be accepted when the end of the input is
reached and a final state is on top of stack;
error, otherwise.
These actions of the parser are supported by the parse table, Le. the action-table
and the go to-table. The principle algorithm is given in Figure 2.8.
The construction of the parse tables is not a trivial task and the explanation of the
appropriate mechanisms is beyond the scope of this text. For more detailed infor-
mation and examples see [AHOS 86] or [TEUF 89].
Semantic Analysis
Lexical as well as syntactical analysis are not concerned with the semantic mean-
ing of programs. But a compiler has to check not only the syntactical correctness of
a given source code, it has also to check, whether the semantics correspond to that
of the programming language. This means that semantic analysis has to guarantee
that all context sensitive rules of a programming language are considered.
postfix notation,
two-address code.
Code Generation
The function of a code generator is basicly the translation of the output from the
syntax and semantic analysis (Le. the intermediate code) into an equivalent se-
quence of instructions which can be executed on the target machine. Two essential
requirements exist for the output of a code generator:
2.3 Compiler Aspects 39
Clearly, the first requirement is a necessity, while the second requirement cannot
be reached fully (in general). Since the generation of optimal code is a NP-com-
plete problem, it is possible to generate high quality code, but not necessarily opti-
mal code.
When generating machine code on the basis of an intermediary code it can be as-
sumed that all necessary semantic checks were carried out. That means that type
checking and most of all type conversion has already taken place and, therefore,
that the code generator must not take care about all the semantic rules of the pro-
gramming language. But the conclusion can by no means be that code generation
is an easy process. In general two basic decisions must take place when generat-
ing code.
Additionally to these two basic decisions which are common to nearly all code
generators, a few other important things must be decided when generating code for
a target hardware. Among them are the selection of the addressing mode as well
as fixing the order of evaluation. For instance, the instruction set of a target ma-
chine probably allows just a subset of all possible addressing modes, as the ADD
instruction of the Motorola 68000 processor, which does not allow memory-to-
memory addition (Le. either the source or the destination of this instruction must be
a data register). Thus, the selected addressing mode may limit the available in-
structions in a certain case.
40 2 Language Processing
The languages differ in their run-time environments depending on the features they
provide. For example, FORTRAN does not allow recursively defined procedures or
data structures and, therefore, the memory requirements for any FORTRAN pro-
gram can be determined at compile time. This is not possible in languages such as
ALGOL 60, PASCAL or MODULA-2, since they allow recursively defined structures
which require dynamic memory allocation techniques. However, those languages
usually combine stack allocation and heap allocation techniques.
Storage Organization
Assuming that the compiler can use a particular block of memory for a compiled
program, then the run-time storage might be organized as shown in Figure 2.9. The
code section contains the generated machine instructions. The size of this section
can be fixed at compile time. The size of some of the data objects may also be
known at compile time and, therefore, they can also be placed in a fixed memory
location, the static data section. Such data objects exist during the whole lifetime of
the program. All these locations can be statically allocated since they do not
change during program execution. The advantage of statically allocated data ob-
jects is obvious: their addresses can be coded into the machine instructions.
The stack is used to handle the activations of different program units (e.g. proce-
dures). The information which is needed to execute a program unit is stored in so-
called activation records (see below). The activation of a program unit causes the
interruption of the currently executed program unit and its actual status is saved on
the stack, which then can be restored after the termination of the called unit.
The heap is that storage area which in languages, such as PASCAL, is used to al-
locate memory for data objects (or program units) which are generated during run-
time in an unpredictable way, i.e. in cases where the number and lifetime of such
objects cannot be represented by a static activation structure. The size of both,
stack and heap, can change during program execution which is represented in
Figure 2.9 by their opposite position. Both can grow towards each other.
2.4 Run-time Environments 41
Code Section
Stack
+ +
t t
Heap
Activation Records
Program units usually need some information for their execution. All this informa-
tion is treated as a data block which is called activation record. An activation record
is pushed onto the run-time stack when a procedure or, generally speaking, a pro-
gram unit is called (Le. activated), and it is popped off the stack when the procedure
is terminated. A procedure's activation record could consist of the following infor-
mation:
Parameters and results: Space for actual parameters and possible re-
sults.
Machine status: Information about the machine status just before the pro-
cedure call, e.g. values of the program counter and registers. This infor-
mation must be restored when terminating the procedure.
Dynamic link: A link to the activation record of the calling instance. This
control link is given as the base address of that activation record and it is
used to reorganize the stack when terminating a procedure.
42 2 Language Processing
static allocation,
heap allocation.
Using static storage allocation techniques the data objects are bound to their
memory locations at compile time. This technique can be applied, if the compiler
knows exactly the number of all objects, as well as the type and therefore the size
of all objects. Obviously, languages using static storage allocation do not allow ei-
ther dynamic data structures nor do they allow recursive procedure calls.
These techniques introduced in this Section are explained in more detail in the
following Chapters when discussing certain implementation techniques.
3 Data Types
A data type is a set of data objects together with an appropriate set of operations for
the object's manipulation. Programs are usually written to perform certain actions
and, in doing so, to manipulate some kind of data objects in certain ways. Thus, the
possibilities of data representation by the features of a programming language are
very important for a programmer. Programming languages partly differ from each
other by the allowed types of data and the appropriate operations on data objects
of these types.
The subject of this Chapter is to discuss the specification of data types and the con-
cept of binding, followed by an introductory to elementary data types, structured
data types, as well as abstract data types. Type checking, the implementation of
data types and a consideration of variables - the carriers of certain types - will fi-
nally close this Chapter.
3.1 Specifications
Data objects on the physical level of a computer can be seen just as a sequence of
bits with no distinction between the meaning of the different objects. The introduc-
tion of types allows a classification of data objects on a higher level which, then,
can be projected onto lower levels by the compiler. The classification of data ob-
jects is the same as in mathematics where we classify variables according to cer-
tain characteristics. In mathematics we distinguish between integer, real and com-
plex variables, for example. We specify spaces for the variables of these types and
we declare certain operators and functions on these types. Thus, data classification
in computing is closely related to those classification methods in mathematics.
Elementary Level
integer
real
boolean
:>.,
char
><:
Q)
Q.
E Structured Level
0
0
...... array
0 record
Q) list
U)
ra
Q)
....
(,)
-c:
Abstract Level
Structured level: Most high level programming languages allow the de-
finition of structured types which are based on simple types. We distin-
guish between static and dynamiC structures. Static structures are arrays,
records, and sets, while dynamic structures are a b it more complicated,
since they are recursively defined and may vary in size and shape during
the execution of a program. Lists and trees are dynamic structures.
Abstract level: Programmer defined abstract data types are a set of data
objects with declared operations on these data objects. The implementa-
3.1 Specifications 45
Now, specifying a data type means to define the type's characteristics, its value
range, as well as its operations (cf Figure 3.2). Obviously, a good name for a type
must also be found to refer to these entities. The characteristics of a type specify
certain properties which data objects (e.g. variables) of that type have , e.g. if the
objects have a fixed-point representation or a floating-point representation . Thus,
the characteristics of a data type determine the representation of that type on the
computer.
In mathematics we are used to have a clear defined range of values for a specific
type, even if this range is infinite. However, on a computer we won't find an infinite
range of values for a data type, but we have to establish the set of all possible val-
ues which can be taken by a data object of a specific type. For example, the type
integer provides a subrange of all integers, which is usually determined by the
greatest and least integer that can be represented on a certain hardware. For ele-
mentary data types the range of values is given by a set with a order relation de-
clared on this set, i.e. for any two elements x and y of the set, either x ~ y or y ~ x.
For the manipulation of data objects of a specific type we have to define certain op-
erators. Again, we find a close relationship to mathematics. Analogically to mathe-
matical functions, a domain and a range is defined for each operator, and the op-
erator describes a mapping from the domain (or the operator's arguments) onto the
range (or the operator's appropriate result) . This mapping can be given as an al-
gorithm.
Considering operators, such as +, -, *, t, we know that the meaning of these ope ra-
tors depends or, their context; we call this overloading. For example, both integer
and real addition are simply expressed by +. It is the task of the compiler to deter-
mine the types of the arguments and, then , choosing the correct machine instruc-
46 3 Data Types
tions. Thus, it is impounded that, for instance, integer + integer results in an integer
and real + real in a real.
By the last example we implied also the conversion of types when considering
integer + real, for example. We should not forget such problems when specifying a
data type. Two methods of type conversion exist: ImpliCit conversion and explicit
conversion. For example, PASCAL allows the multiplication of a real with an inte-
ger data object and the conversion is implicitly done by the compiler. Not so
MODULA-2, here we have (i.e. the programmer has) to convert explicitly the integer
variable into a real variable before the multiplication can be executed.
We distinguish between static and dynamic binding according to when the binding
occurs: Dynamic binding is done and can be changed during run-time, while static
binding is done before run-time and is not changed during the program execution.
Bindings can take place at different times:
variable's type, and the type can change with another assignment. For
example, in APL a variable A is of type integer after the execution of the
assignment statement A f- 10, after the execution of A f- 10.1, A is a
floating-piont variable.
integer,
real,
boolean, and
char
can be found in nearly every (imperative) language. Since the numeric data types
are highly dependent on the hardware representation of the numbers and the im-
plementation of the arithmetic, their usage can cause different results on different
computers.
Integer
Integer is the simplest numeric data type of the built-in types of programming lan-
guages. Usually integers are represented in a word or a set of words of the com-
puter, where the leftmost bit indicates the sign. For example, an 8-bit word can be
used to represent values in the range -128 to + 127, using the 2's complement.
Thus, the word length of the computer determines the range of integer values (cf
Table 3.1).
48 3 Data Types
We are often allowed to define long integers to enlarge the value range of the inte-
gers. In this case integers are represented by two or more words. The basic arith-
metic operations on data objects of type integer are mapped onto the hardware's
fixed-point arithmetic operations.
Real
The data type real is used to model real numbers. In contrast to the data type inte-
ger, where we can represent each integer number within a range, the representa-
tion of each real number within a certain range is not possible. In mathematics, for
example, ~ is exact and we can work with this real number. But on a computer
this real number cannot be correctly represented (the same counts for such impor-
tant numbers like JT: or e).
The representation of real numbers and most of all the operations on real numbers
are an awkward task. The basic operations, such as addition and multiplication,
must be done with well-defined roundings, i.e. a computer arithmetic must be
based on a mathematical theory [KULI 81], [TEUF 84]. Unfortunately, there exist a
great number of computers where these requirements are not considered. There-
fore, serious problems can occur when executing a program on another hardware.
I I
Sign Exponent Mantissa
o 11 12 63
As for integers, most of the programming languages provide data types, such as
long real. With long reals we have a larger mantissa (e.g. double the length), but
this does not mean that we automatically get an increase of accuracy for all compu-
tations. The problems and risks with insufficient implemented computer arithmetic
are obvious.
Boolean
The boolean data type is the simplest built-in data type of (imperative) program-
ming languages. Its value range consists of only two elements: true and false.
Standard operations bound on the boolean data type are the logical operations
and, or, and not. For the comparison of boolean values we usually find false con-
sidered to be less than true (e.g. in PASCAL, MODULA-2, etc.). Obviously, boolean
data objects theoretically can be represented by a single bit. But the values are
bound to the smallest addressable element which is normally a byte. The opera-
tions are also bound to certain machine operations.
Character
The value range of the data type char (or character) is bound to a set of characters
which can be a defined enumeration or a standard set, which, then, is mostly the
ASCII character set. ASCII (American Standard Code for Information Interchange)
stands not only for a character set, but also for a code allowing the representation
of characters by a sequence of bits. The ASCII character set contains not only the
digits and the elements of the alphabet, but also a number of control characters.
Each character of the ASCII character set is represented by a unique 7-bit pattern.
Thus, the representation of char data objects is usually given by bytes (8 bit), where
the eighth bit is either unused or used as a parity bit. Another standard code which
is mainly used on IBM computers is the 8-bit code EBCDIC (Extended Binary
Coded Decimal Interchange Code).
An order is defined on the character sets, usually the alphabetic order. This order
determines the relational operators, such as <, ~, =, ~, >, which are bound to the
data type char.
50 3 Data Types
The previously introduced built-in data types allow only the representation of the
basic elements of the real world. But real world problems normally require a more
structured way of defining data. Imperative programming languages, such as
PASCAL, MODULA-2, or ADA, allow for this reason the definition of user defined
(structured) types applying the concept of orthogonality. This means that - on the
bases of some elementary constructs - flexible composing mechanisms are avail-
able. The definition of such structured data types is mainly based on well-known
mathematical composing methods, like
cartesian products, or
sets.
In this way the elementary data types can be used to define data objects with a
higher complexity, i.e. as an aggregate (or unit) of elementary objects. Modern pro-
gramming languages normally allow the naming of such complex structures. Thus,
the programmer can define type names for aggregations of elementary or user-de-
fined types, and thereafter, he can declare arbitrary variables or data objects to be
of this type (similar to the declaration of simple type data objects).
defines a one-dimensional array of name a, the elements of which are of type real
and the index range is a subrange of the integers, i.e. the index of the first array
3.4 Structured Data Types 51
element is a and that of the last element is 9. The access of an array element (in
MODULA-2, other languages may use a slightly different syntax) is given by
ark]
An interesting question is, whether the array bounds should be constant during a
program execution, or whether they can be changed dynamically. While in
PASCAL array bounds are constants, ADA or SIMULA 67, for instance, allow dy-
namic indexing ranges. Obviously, constant array sizes are simpler to implement
than dynamic ones which need more expensive dynamic storage allocation
techniques.
The implementation of fixed-size arrays is not very difficult, since the size of the ar-
ray is known at compile-time. The access to an element a[k] of a one-dimensional
array a[11 .. u1] is given by the address
where base a denotes the address of the first element of the array (Le. a[11]) in the
data area, 11 (u1) denotes the lower (upper) bound of the array, and size is the size
of storage (counted in words or bytes), which must be allocated to store a single
element of the array (depending on the array's type, of course).
row-major form, or
column-major form.
a 13,21
a [3,31
a [3,4]
a [3,5]
a [4,2]
a 14,31
a [4,4]
a 14,51
Row-major form means that the array elements are stored with the rightmost index
varying the most rapidly, while in column-major form the array elements are stored
with the leftmost index varying the most rapidly. Figure 3.4 shows a two-dimen-
sional array a[3 ..4, 2.. 5] stored in row-major form.
The address of an element ali, j] of a two-dimensional array a[11 .. u1, 12 .. u2] is then
given for row-major forms as
In the case, where both lower bounds are 0, these address formulas can be simpli-
fied to
respectively.
Using these formulas, we can determine the address of the array element a [4, 4]
(cf Figure 3.4) as
The handling of dynamic arrays (Le. arrays where the upper and/or lower bounds,
and hence the size, of the array are only known at run-time) is a bit more complicat-
ed. In general, the problem is solved by generating an array descriptor at compile-
time, which then is initialized at run-time. The descriptor reserves space to hold the
information about the array (e.g. number of dimension, upper and lower bounds),
while space for the array elements itself is allocated form the heap during program
execution. The addressing of the elements of a dynamic array is the same as for
fixed-size arrays, but the upper and lower bounds must be taken from the descrip-
tor.
3.4 Structured Data Types 53
Records
defines a record of name NGFreqs, the elements of which are of type real, cardinal,
and an array of characters. Then, the type NGFreqs is instantiated by declaring the
variable ngram. The access to an element (say rf) of this structure (in MODULA-2,
again other languages may use a slightly different syntax) is given by
ngram. rf I
i.e. the selection of record components is done by names which are known at
compile time. This is different to the selection of array elements, where the index is
normally calculated during the program execution. This form of referencing a com-
ponent of a record is called qualified name form and is used in PASCAL,
MODULA-2, and ADA, for example. ALGOL 68 is an example for a language which
references components by the so-called functional notation, e.g. ngram (rf) .
The implementation of records is fairly simple and very similar to fixed-size arrays.
A record's size is known at compile time and the access to a field of a record, say
r.k, can be done via a base address (baser) and an appropriate offset:
k-1
baser + L size(r.i) ,
i=1
Variant Records
Variant records are specified to have a choice between different, alternative struc-
tures. They are used to structure the data objects of problems which have a great
many elements in common and differ in certain components, or, for example, if
problems can be structured in different ways. An example for the latter is a type
defining figures that can be either circles or rectangles:
The component which determines the variant actually used (in our example kind)
is called type discriminator or tag field. In the given example a circle is given by its
radius, and a rectangle by its length and width. The implementation of variant
records is in general straightforward, Le. storage space is allocated for the largest
possible variant.
Two major problems exist with PASCAL and MODULA-2 variant records. First it is
possible to omit the tag from the variant record which makes it impossible to de-
termine the current variant type. Thus, improper assignments become possible.
The other problem is that a program can change the tag, without changing the vari-
ant in an appropriate way. This leads to inconsistent states and, therefore, can
cause run-time errors.
Pointer
The first of these concepts can be found, for example, in PASCAL, MODULA-2, or
ADA. Pointers are only allowed to reference data objects of a single type, which is
determined with a pointer's declaration. The binding of a type to a pointer variable
distinguishes high level language pointers from addresses in assembly languages.
This binding has the advantage that a strong typing concept (and, therefore, static
type checking) can be applied. In MODULA-2, for example, a pointer declaration
can be given in the following way:
defining a pointer to data objects of the above defined structured type NGFreqs
which consists of a character array, a cardinal, and a real field.
The concept that a pointer can reference data objects of any kind can be found in
SNOBOL4 or PU1, for example. Static type checking is impossible if a pointer is
not restricted to reference data objects only of a single type. This increases the
probability for unreliable program structures using pointers. Therefore, modern
high level programming languages are mostly based on the concept that pointer
variables are bound to a single type.
NgramTree := nil;
One characteristic of recursive data structures is the ability to vary in size and, thus,
that a compiler cannot statically allocate storage for the components of such struc-
tures. The compiler allocates only memory to store the value, i.e. an address, of the
56 3 Data Types
pointer variable. Storage for the structure to which the pointer points is allocated
dynamically during program execution, Le. whenever a new element of such a
structure is to be generated, memory is dynamically allocated from a storage area,
which is usually called a heap. For this, we normally find an intrinsic function like
new(NgramTree);
This statement effectively allocates storage for a record of type NGFreqs and initial-
izes the pointer value by the address (absolute or relative) of the data object (Le. a
dynamic variable).
Pointers can be used to reference either to the contents of the memory location to
which the pointer variable itself is bound (Le. an address), or it can be used to ref-
erence to the contents of the memory location whose address is the contents of the
location to which the pointer variable is bound. We call the latter pointer derefer-
encing. In MODULA-2 or PASCAL dereferencing is done using the symbol ,w', e.g.
NgramTree".
The concept of dereferencing is shown in Figure 3.5, where the address of the
pointer variable NgramTree is assumed to be 1000 and its value to be 2000. A
normal reference to NgramTree yields 2000, while a dereferenced reference al-
lows access to the contents of the memory location with the address 2000, e.g.
NgramTree".ng := 'abcdef';
writes the string' abcdef' into this location. Obviously, we have to use some off-
sets to address the different fields of the structure. The assignments
NgramTree".tf .= 0;
NgramTree".rf .= 0.0;
NgramTree".left .= nil;
NgramTree".right := nil;
will initialize the rest of the node which was generated by the above new operation.
Figure 3.6 shows the according binary tree after these assignments.
Along with the new operation we find usually a dispose operation to return explic-
itly allocated memory dynamically (in languages like MODULA-2 or PASCAL). For
example (again in MODULA-2 syntax),
dispose(NgramTree);
returns the memory pointed to by NgramTree to the storage pool. The value of the
pointer variable becomes undefined and the data formerly associated with
NgramTree" are no longer accessible. The memory returned to the storage pool
can then be reused later when another new operation is executed. In other lan-
3.4 Structured Data Types 57
1000
NgramTree
I 2000 I
~
2000
,
----- ... -.. - ... ---
--- __ - - - abcdef
-
NgramTre e".ng 'abcdef' ;
Data Object
of Type
NGFreqs
NgramTree
••--~----~~~I abcdef
tJ1
Q
The new and dispose operation for the allocation or deailocation of memory are the
critical issues in the implementation associated with pointers. Dynamic storage al-
location techniques are required along with a storage management system (for
more details see Chapter 3.7).
58 3 Data Types
Sets
defines a set type DigitSet based on the subrange type Digits, as well as the
two set variables even and odd.
The possible values of a set variable are the elements of the powerset of the base
type B. The set of all subsets of a given set B is called the powerset of B. This pow-
erset contains B itself, the void set 0, and - if Bs cardinality is greater than 1 -
2 cardinality(B) - 2
subsets S, satisfying
o e s c B, S * 0, and S * B.
For instance, the powerset of of a set {a, b, c} has the six subsets {a}, {b}, {c}, {a, b},
{a, c}, and {b, c}. Set variables are usually, like other variables, initially undefined.
The MODULA-2 assignment statement
The basic operations which are bound to set types are the following:
Set intersection ("*"): The intersection of two sets S 1 * S2 is the set of all
those elements common to both S1 and S2-
Set union ("+"): The union of two sets S1 + S2 is the set of all those ele-
ments which belong either to S1 or to S2 (or to both).
Set difference ("-"): The difference of two sets S 1 - S 2 is the set of all
those elements which belong to S1 but not to S2.
Set insertion: Inserts a value in a set, provided that the value is not al-
ready an element of the set.
Often there is no maximum size of the cardinality of a set defined by the language
(like in PASCAL or MODULA-2), but the implementations of these languages
usually define limits for the size of sets, which can be quite small (e.g. the number
of bits in a word or a small multiple of words). This makes the implementation of set
types as bit strings very simple. Then, the operations on sets are usually directly
supported by the hardware.
Procedure Types
Although procedure types do not appear in any of the common procedural lan-
guages - except in MODULA-2 - they should be introduced here, since they repre-
sent a very powerful concept. The introduction of a procedure data type allows the
consideration of procedures, like data values, as objects that may be assigned to
variables. A procedure type declaration specifies
the types and variable or value status of the parameters of procedures (or
functions) of that type;
For example, we define a procedure type P 1 with two cardinal value parameters
and one real variable parameter in MODULA-2 in the following way:
Any procedure with such a parameter structure is of type P 1. Variables of this type
can be declared and assignments can be made to such variables. A procedure
variable can be called (or activated) in exactly the same way as the procedure it-
self. But the major use of procedure types is to enable the passing of procedures or
functions as parameters. This can be very important in certain situations where dif-
ferent procedures are required to operate on similar contexts.
File Types
Files are still this medium for the I/O of huge amounts of data and they can act as a
communication link between different applications. Programming languages nor-
mally provide a file type to perform the management of files, i.e. the creation, the
read/write access, and the deletion of files. Along with these operations we often
find operations to open and close a file, and to specify a file's access attributes.
Such access attributes are read-only, write-only, or read-write.
Sequential files, the simplest, but probably most important structure. The
components of a sequential file can be accessed only in linear order, i.e.
at any time only a single component of the file is accessible. The compo-
nent is specified by the current position of the access mechanism. Since
secondary storage still depends on some forms of mechanical movement
- the basic characteristic of which is a sequential behaviour - the impor-
tance of sequential files becomes obvious.
In the previous section we have seen that various possibilities exist to define struc-
tured data types to express real world problems. But we know that complex real
world problems do not require only an abstraction in terms of data structures but
3.5 Abstract Data Types 61
also in terms of operations on data objects of such structured types. This means,
that programming languages should provide constructs for the definition of abstract
data types.
The fundamental ideas are that data and the appropriate operations on it belong
together, and that implementation details are hidden to those who use the abstract
data types (Le. the concept of information hiding as described by Parnas [PARN
72]). Thus, the features which must be provided by programming languages to al-
low the definition of abstract data types can be summarized as follows:
Features to hide implementation details about both the objects and the
operations.
The concept that data and the appropriate operations on it should form a syntactic
unit (i.e. the concept of abstract data types) was initially introduced with
SIMULA 67 and its class construct. Other languages followed, such as MODULA-2
and OBERON with the module construct, ADA with the package construct, or CLU
rUSK 81] with the cluster construct.
As an example for an abstract data type we show how a queue and operations on it
can be promulgated using MODULA-2, which distinguishes between definition
modules - acting as user interfaces representing types and operations - and the
implementation modules hiding all details about types and operations. A possible
MODULA-2 definition module could then be:
END QueueHandler.
Now, programmers who want to use the abstract data type import the type and op-
erations from the given user interface, i.e. the definition module. They do not need
to know anything about the implementation. The example shows, that MODULA-2
allows the declaration of so-called opaque types by which it is possible to hide the
type's implementation.
The shown concept is the same as for elementary data types. For instance, con-
sidering a data object of type real. We are interested in the usage of such a data
object and not in its implementation, i.e. we clearly distinguish between usage and
implementation. This differentiation allows the user to consider an object in an ab-
stract way, no matter whether it is based on an elementary data type or an abstract
data type as introduced above.
We all know that, for example, the multiplication of a boolean with a real data object
makes no sense. Inconsistencies of that kind can be recognized by certain type
checking mechanisms. Type checking is the practice of ensuring that data objects
which are somehow related are of compatible types. Two objects are related
Consistency checks which are made before the execution of a source program (i.e.
by the compiler) are said to be static checks, while those checks performed during
the execution of a source program are called dynamic checks (or run-time checks).
Checking the syntax is an example for static checks, while type checks are an ex-
ample of checks which often can be done statically, and which sometimes must be
done dynamically.
is allowed, i.e. whether both sides of the assignment statement are of compatible
types. Static type checking is usually supported by so-called symbol tables (see
[TEUF 89], for example).
For the implementation of data types we have to consider, how data objects of a
particular type can be represented on the computer, and how the appropriate op-
erations can be realized.
The storage representation of integer and real types is usually supported by the
hardware, i.e. the number representation of the computer system is used to repre-
sent fixed-point and floating-point numbers. If the target machine WOUldn't provide,
for example, a floating-point representation of numbers, the representation had to
be simulated by software, what, obviously, becomes more expensive. The arith-
metic operations also directly correspond to the fixed-point and floating-point
arithmetic of the computer.
mapped onto integer values. Since a byte is often the smallest addressable unit on
a computer, not a single bit, but a byte is used to store boolean data objects. The
interpretation could be, if one bit is 0, the data object is said to be false and it is true
if all bits are 1. Logical operators are usually supported by the hardware.
The representation of data objects of type character are most often supported by
the operating system and/or hardware. Normally we find a representation which is
based on some standard character sets.
For example, the two-dimensional array a[3 ..4, 2 .. 5] of Figure 3.4 could be imple-
mented by an descriptor followed by the array elements in adjacent memory loca-
tions as shown in Figure 3.7. Now, in this representation the base address (basea)
is the address of the memory location containing the number of dimensions.
~ 2 number of dimensions
Fig. 3.7. Possible representation of the array a[3 .. 4, 2.. 5] using row-major form
3.7 Implementation of Data Types 65
Analogically, the descriptor of a record type could contain the number of fields, in-
formation about the fields (e.g. upper and lower array bounds), and so on.
In general, heap allocation is not very complicated, but a requirement is the avail-
ability of a heap management. Since it is possible to deallocate storage in any
order, strategies to manage and to combine the free storage blocks are necessary.
Free storage blocks are usually linked together in a free-list (cf Figure 3.8).
stack
p1
p2
p3
The type of a variable specifies the range of values which can be associated with
that variable, and it specifies the set of operations which can be applied to manipu-
late variables of that type. For example, the values of variables of type boolean can
be true or false and the allowed operations on such variables are given by the logi-
cal operators of a programming language. The binding of a value to a variable is
dynamic, since the value can be changed during program execution by assignment
of new values. In contrast to this, constants can be seen as static variables not al-
lowing their value to be changed during program execution.
The discussion about scope and lifetime of a variable became important especially
in block-oriented programming languages. The scope of a variable is described by
the range of statements within the program over which the variable can be manipu-
lated and accessed by its name. The lifetime of a variable is described by that part
of the program's execution time in which the variable is bound to a memory loca-
tion. Considerations about both, scope and lifetime, are substantial, since they de-
termine the way in which a variable (or also other entities of a program) can have
effects. By effects we mean, for example, the manipulation and accessibility of a
memory location using a variable's name.
3.8 Variables: Scope and Lifetime 67
The difference between scope and lifetime of a variable can easily be exemplified
considering, for example, a SIMULA 67 program consisting of several blocks. A
block in SIMULA 67 (similar to ALGOL 60) is a unit consisting of declarations and a
succeeding set of statements which together are treated as one statement. There-
fore, those declarations and statements are labeled by a begin-end pair. In
SIMULA 67 a block may be introduced at each point of a program where a single
statement is allowed. An example for such a program is given in Figure 3.9, while
the appropriate scopes and lifetimes of the variables are given in Figure 3.10.
BEGIN
REAL X, Y;
INTEGER I, J, K;
BEGIN
REAL Xl;
INTEGER A, J;
BEGIN
REAL X;
INTEGER A, K;
END;
BEGIN
REAL A, B;
INTEGER H, I;
END;
END;
END;
The variables which are declared in a certain block are said to be local to this
block. Figure 3.10 shows that the lifetime of the variables of a block correspond to
the blocks lifetime. It can also be seen how scope and lifetime of a variable of a
block differ. For example, variable J of block 1.1 is bound to a memory location
during the whole lifetime of block 1.1, while it is not known in the inner blocks, i.e.
block 2.1, block 3.1, and block 3.2, since there is another variable declared with the
same name in block 2.1.
x y I J K Xl A J X A K A B H I
B1.1 sl sl sl sl sl
sl sl sl sl sl
B2.1 sl sl sl 1 sl sl sl sl
sl sl sl 1 sl sl sl 81
1B3.1 1 81 81 1 1 sl 1 81 81 81 81
1 81 81 1 1 81 1 81 81 81 81
sl sl sl 1 sl 81 81 sl
IB3.2 sl sl 1 1 sl 81 1 sl 81 sl sl sl
sl sl 1 1 sl sl 1 sl sl 81 sl sl
sl sl sl 1 sl sl sl sl
sl sl sl sl sl
Fig. 3.10. Scopes (s) and lifetimes (1) of the variables of Figure 3.9
4 Expressions and Control Structures
According to Wirth expressions can be defined informally as follows [WIRT 76]: "An
expression consists of a term, followed by an operator, followed by a term. (The two
terms constitute the operands of the operator.) A term is either a variable - repre-
sented by an identifier - or an expression enclosed in parentheses." This definition
is slightly inexact, since it considers only binary operators. On the other hand, this
definition shows that our above given definition is incomplete in the sense that
parentheses and most of all recursion are not mentioned. In fact, expressions rep-
resent the simplest examples of recursively definable objects.
The most important question in the evaluation of expressions is in which order the
evaluation is done. For example, if we consider the arithmetic expression
x + Y * Z - 1
it is from a pure mathematical point of view clear what the result must be. Assuming
x, Y, and z to be 1, 2, and 4, respectively, the result of the expression is 8, because
we first multiplied Y by z and then added x and subtracted 1. We implicitly followed
some priority rules for mathematical operations. We can get totally different results,
if we do not have such priority rules. Other results of the given arithmetic expres-
sion could be 7, 9, or 11, depending on the order of evaluation.
exponentation,
relational operators,
negation,
logical and,
logical or.
4.1 Expressions and Operators 71
Other languages may use slightly different priority rules. A comparison of the
precedence of operators in the languages FORTRAN, ALGOL 60, PU1, PASCAL
and ADA can be found in [HORO 84].
Boolean expressions differ from arithmetic expressions in that way that the result of
a boolean expression can probably already be determined by knowing the value of
one operand. For example, the boolean expression
A OR (B AND C)
(B AND C)
must not be evaluated if A is already true. A similar situation can be found if the two
logical operators are exchanged:
A AND (B OR C)
(B OR C)
where the left hand operand is always evaluated first and the right hand operand is
only evaluated if it is necessary in order to determine the result. In MODULA-2 we
find only short-circuit evaluations, while the evaluation of boolean expressions in
PASCAL is implementation dependent.
72 4 Expressions and Control Structures
Explicit control structures allow the programmer to replace the implicit, i.e. default,
control structure of a programming language. Sequential machines have the im-
plicit control structure that in a sequence of statements, one after another is execut-
ed. Now, programming languages provide, for example, selection and repetition
statements to modify explicitly the default sequence of execution, or procedure call
mechanisms to allow the programmer to define the control flow between program
units.
The IF statement was already introduced in the early FORTRAN versions, but only
in a very simple form:
where e represents the (arithmetic) condition and LI, L2, and L3 are labels. The
semantics of this statement is that we branch to the statement with the label LI, L2,
or L3 depending on whether the result of the evaluation of condition e is negative,
zero, or positive, respectively.
true false
statements 1
statements
where the ELSE clause of the statement must not necessarily exist. Figure 4.1 rep-
resents this structure diagrammatically. The semantics of such an IF statement is
that the alternative described by statements 1 is executed if the value of
expression is true, otherwise the alternative described by statements 2 is exe-
cuted. For example,
means that if x = 0.0 we increment x by 1 and in all other cases we assign the
value y/x to x.
Clearly, statements 1 can again be a conditional statement, which then can lead
to the following well-known ambiguity problem:
Now, it is not clear whether the statements S2 of the ELSE clause belong to the IF
statement with the condition E 2 or to the IF statement with the condition E 1. The
ambiguity can be shown by considering the following grammar, which allows the
generation of two different parse trees (cf Figure 4.2) for the above given IF state-
ment:
stmt ~ IF expr THEN stmt I IF expr THEN stmt ELSE stmt I other.
stmt
~
IF expr THEN stmt
I
y
E1
IF expr THEN stmt ELSE stmt
I
I I I
Y Y Y
E2 S1 S2
stmt
,
IF expr THEN stmt ELSE stmt
I
Y
E1 S2
, ,
IF expr THEN stmt
E2 S1
In MODULA-2 every control structure (and, therefore, also the IF statement) has an
explicit termination, which is usually an END. Thus, in MODULA-2 the above given
ambiguity problem does not occur, because with the placement of the END's it is
clear which IF statement goes with which ELSE clause:
Similar approaches can be found in ALGOL 68, where IF statements are terminat-
ed with a FI, or in ADA, where the termination of an IF statement is an END IF.
IF E1 THEN Sl
ELSE IF E2 THEN S2
ELSE IF E3 THEN S3
ELSE IF Ek THEN Sk
ELSE
S(k+1)
END
END
END
END
we usually find some form of an ELSIF construct, which avoids the writing of all of
the END's:
IF E1 THEN Sl
ELSIF E2 THEN S2
ELSIF E3 THEN S3
ELSIF Ek THEN Sk
ELSE S (k+1)
END
CASE expression OF
CaseLabelListl Sl
CaseLabelList2 S2
CaseLabelList3 S3
CaseLabelListk Sk
ELSE S (k+1)
END
Other languages may provide a slightly different syntax for CASE statements. For
example, not every PASCAL implementation provides an ELSE clause in a CASE
statement and, if it is provided, it is usually called OTHERWISE. In ALGOL 68 the
expression is of type integer, while ADA names the ELSE clause OTHERS and
uses WHEN for the "I" in MODULA-2. In C the CASE statements are called
SWITCH statements, the expression is of type integer, the term CASE is used like
WHEN or "I", and the ELSE clause is called DEFAULT.
The requirement for repeating several statements within a program is a quite often
occurring situation. A very simple example, which can be found in most programs,
is the initialization of an array variable. Repetition statements like
4.4 Repetition Statements 77
FOR statements
allow a programmer to code such repetitions. These two groups of repetition state-
ments are commonly distinguished as counter-decided and condition-decided, re-
spectively.
Again, already in the early FORTRAN versions we can find a very simple form of a
counter-decided repetition (although it does not contain the keyword FOR), e.g.
DO 111 I = 1,20
X(I) = 0.0
111 CONTINUE
where 111 is a label with a CONTINUE statement terminating the repetition, I is the
counter variable with the lower bound 1 and upper bound 2 o. All statements be-
tween the DO statement and the CONTINUE statement are repeated as many times
as it is specified by the lower and upper bound for the counter variable.
where the BY clause must not necessarily exist. Fig 4.4 represents the principle
structure of FOR statements diagrammatically. The semantics of such a FOR state-
ment is as follows:
no
cv in range
no
cv < FinExpr
As usual different programming languages vary in the syntax for FOR statements,
but there are also some differences in the semantics of FOR statements. The most
important differences are
whether the counter variable holds a defined value after the termination
of the FOR statement,
whether FinExpr and IncrExpr are evaluated only once, or each time
the counter variable is checked against the final value and incre-
mented/decremented.
In PASCAL and MODULA-2 the counter variable is undefined on exit from the FOR
loop, while in ADA the counter variable only has the scope of the FOR statement
and, thus, is not available outside. In ALGOL 60 the counter variable is only unde-
fined if the FOR loop was normally terminated, and FinExpr and IncrExpr are re-
4.4 Repetition Statements 79
evaluated each time they are needed. This is not done in languages like PASCAL
or ADA.
Counter decided repetition can easily be applied in all those cases, where the
number of repetitions is more or less known in advance (e.g. for some form of ini-
tialization). However, there are a lot of problems requiring an indefinite looping, i.e.
the number of repetitions is not known in advance, but depends on some condi-
tions (e.g. a search in a tree structure). For those cases condition-decided looping
is provided.
false
expression
where the explicit termination of the statement is typical for MODULA-2 or ADA
(using END LOOP). The flow of control for WHILE statements is shown in Figure 4.5.
The semantics of WHILE statements is obvious. The condition is evaluated and the
body of the WHILE statement is executed as long as the condition is true. Thus, the
loop body may be entered zero or more times.
We see that the REPEAT statement in MODULA-2 is not terminated with an END
clause, but with the UNTIL clause. Figure 4.6 shows the appropriate flow of control
diagrammatically.
expression
true
The loop is terminated using the EXIT clause within statements, e.g.
Obviously, it is possible to specify several conditions to exit from the loop. An infi-
nite LOOP statement can replace both the REPEAT and the WHILE statement de-
pending on when the condition will be tested for exiting the loop. Thus, the control
flow of the LOOP statement as shown in Figure 4.7 can be understood as a combi-
nation of Figure 4.5 and Figure 4.6.
Obviously, we program a WHILE statement using the LOOP construct if the ex-
pression is evaluated first in the loop body, i.e. statements 1 is the empty set. A
REPEAT statement is represented if the expression evaluation is the last state-
ment of the loop body, i.e. statements 2 is the empty set. This implies that
REPEAT and WHILE constructs (or even FOR constructs) are redundant in lan-
4.4 Repetition Statements 81
guages providing the LOOP construct. But the readability as well as the writability
of programs is much greater, having several repetition statements available.
true
expression
exception handling,
coroutines, and
tasks.
The simple PROCEDURE CALL allows the explicit, i.e. programmer defined flow
control between statement sequences (or parts of a program), which is diagram-
matically shown in Figure 4.8. The definition of a procedure assigns a name to a
sequence of statements. This statement sequence can be executed using the lan-
guage's call mechanism together with the defined name. In FORTRAN, for exam-
82 4 Expressions and Control Structures
pie, the execution of subroutines can be controlled using the CALL statement and
the procedures name:
while in PASCAL, MODULA-2, or ADA a procedure is invoked using just the proce-
dure's name. Along with the CALL mechanism we find an inverse one, the
RETRUN mechanism, by which the change of control back to the calling instance is
specified. This mechanism can be either implicit (Le. after the execution of the last
statement of a procedure) or it can be explicit (Le. it might be possible to terminate
the execution of a procedure at any point in the procedure's body, by means of an
explicit RETURN statement).
Main Program
1
CALL PROC A
PROC A
END
the greater is the probability that unintentional situations occur, because it is diffi-
cult to overlook the range of effects.
l
Main Program
PROC A
PROC A /
/
CALL PROC A
----!----
CALL PROC A ----!----
CALL PROC A
----r--- \
RETURN RETURN
END
While the behaviour of simple procedures is a static one in terms of memory allo-
cation (in the simplest case a procedure call can just be replaced by the proce-
dure's body), this is not true for recursive procedures. They are dynamic (Le. it can-
not be determined at compile time how often a procedure calls itself) and, thus,
they require a more sophisticated way of implementation. This will be discussed in
Chapter 5.
The concept of controlling the flow of control in the case of an exception is quite di-
verse in different languages. On the one hand, there are languages, like ADA,
where we find the concept that, for instance, a procedure cannot resume from an
exception, Le. the program part where the exception occurred is simply terminated
after the exception handler terminated. On the other hand, we find the concept, fol-
lowed in languages like PU1, that control is returned to where the exception oc-
curred, Le. some kind of repair can be done by the exception handler, and the pro-
gram part where the exception occurred can be resumed. The latter is usually
harder to understand for a programmer.
The idea of procedures or subprograms is that we transfer control from one part of
a program to another part (Le. to a procedure) using the procedure call mechanism,
and that - based on the return mechanism - control is transferred back to the call-
ing instance after all statements of the procedure have been executed. Invoking a
procedure causes the allocation of storage for local data structures, which is deal-
located with the return to the calling instance.
Now, there are situations conceivable, where the body of a called procedure is only
partly executed when control is transferred back to the calling instance and that the
execution of the rest of the procedure's statements is suspended for a certain time.
For example, the simulation of a single server system consisting of a queue and the
server: Both, the queue and the server are mapped onto a procedure, where the
queue-procedure simulates the arrival process of customers (e.g. a Poisson arrival
stream), and the server-procedure simulates the service process (e.g. a negative
exponential distribution of the service time). Obviously, this is a real life problem
with two parallel cooperating processes, which on a sequential computer must be
simulated in a quasi-parallel way, Le. only one of the processes can be in execu-
tion at a time and control must be swapped between them. Coroutines provide a
synchronization for such parallel cooperating processes.
4.6 Coroutines and Tasks 85
,
Main Program
PROC A
1
CALL PROC A
I ~
END
+ ~~----
RETURN
,
a) asymmetric flow of control
Coroutine A Coroutine B
RESUME B
RESUME A
1
RESUME A
RESUME B
RESUME B
1
b) symmetric flow of control
Coroutines are a form of procedures allowing the mutual activation of each other in
an explicit way. As already mentioned, if control is transferred to a coroutine it exe-
cutes only partially, and suspends a part of the execution for a certain time.
Important is, that a coroutine resumes at that point where it last terminated when
control is transferred to it and, therefore, that coroutines have several entry points.
This is exactly the basic requirement for interleaved processes, such as in the
above given example. While the flow of control for procedure calls is asymmetric, it
is symmetric for the activation of coroutines. This is shown in Figure 4.10, where it
is assumed that the transfer of control can be done using a RESUME statement.
Procedures and coroutines are compared in Table 4.1.
86 4 Expressions and Control Structures
Procedures Coroutines
·• transfer control
execute (more or less)
·• transfer control
execute only a part of the
all statements statements
variables etc.
The most popular language containing coroutine features is SIMULA 67, but there
are a few others, such as BLISS, for example, which provide also coroutine fea-
tures. The language definition of MODULA-2 provides no features for symmetric
control flow, but Wirth proposes with his SYSTEM module [WIRT 88a] sufficient
features for coroutines: PROCESS, NEWPROCESS, TRANSFER. Most MODULA
systems provide these features. Good examples how to use these features can be
found in [GUTK 84], for example.
Any program unit that can be in concurrent execution with other program units is
called a task (note: concurrent execution means not necessarily parallel execution
on parallel hardware, rather a parallelism on an abstract logical level is meant,
which then can also be performed on a single processor). While coroutines are
used to describe interleaved processes with an explicit mutual activation, tasks are
used to describe concurrent processes which can be considered to be more or less
independent, i.e. they perform their activities independently to satisfy a common
goal. Various examples can be found considering producer-consumer problems in
operating systems, where one task produces certain entities and another one con-
sumes these entities. Such examples show that features must be provided to con-
trol communication and interaction between tasks, features allowing the synchroni-
zation of tasks. Several synchronization mechanisms have been developed,
among them are
4.6 Coroutines and Tasks 87
semaphores,
monitors,
message-passing, or rendezvous.
Semaphores were introduced by Dijkstra [DIJK 68a], [DIJK 68b] and were used, for
example, in ALGOL 68, while Brinch Hansen introduced monitors with his
CONCURRENT PASCAL [BRIN 75a]. The ideas of message-passing were intro-
duced by Hoare [HOAR 78] and Brinch Hansen [BRIN 78] and applied in ADA to
describe the synchronization of tasks. These mechanisms for the manipulation of
the flow of control are described in more detail in Chapter 8.
5 Procedures
In the preceding Chapter we have already seen the importance of subprograms (or
procedures and functions) as an instrument to manipulate the flow of control in a
program system. Thus, in this Chapter we should talk about procedures in more
detail. We have to discuss methods for parameter passing, overloading and
generic concepts, as well as implementation techniques. But we want to start with a
brief overview on the basic ideas behind procedures.
Today, procedures are still used to write multiply occurring code only once in a
program, but the concept of abstraction in the usage of procedures has become
more important, since procedures are now used to modularize complex problems.
Procedures are a mechanism to control and reduce complexity of programming
systems by grouping certain activities together into syntactically separated program
units. Therefore, the multiple usage of program code is no longer a criterion for
separating code, rather procedures represent more and more source code which is
5.1 Basic Ideas 89
used only once within a program, but which performs some basic operations. This
,
is shown in Figure 5.1 b.
Main Program
PROG A
1
GALL PROG A
~
GALL PROG A RETURN
GALL PROG A
END
a) code reduction
,
PROG X
Main Program
1
RETURN
GALL PROG X
PROG Y
~
GALL PROG Y
~
GALL PROG Z
1
RETURN
PROG Z
1
END
RETURN
b) complexity reduction
Subprograms are often divided into procedures and functions. In general, the only
difference between them is, that a function is a subprogram producing a result, i.e.
it returns a value for the function name. Thus, a function is a form of an expression
and, therefore, can be used wherever an expression can be used. In PASCAL we
find for these two kinds of subprograms the keywords PROCEDURE and
FUNCTION, while MODULA-2, for example, knows only PROCEDUREs, which can
or cannot produce a result. In C, functions are the only available type of a subpro-
gram. When we talk about procedures in the following, we mean also functions,
unless we explicitly state a difference between them.
Characteristics
• The procedure call mechanism allocates storage for local data structures.
• The procedure call mechanism transfers control to the called instance and
suspends the calling instance during the execution of the procedure (Le.
no form of parallelism or concurrency is allowed).
5.1 Basic Ideas 91
• The procedure return mechanism deal/ocates storage for local data struc-
tures.
• The procedure return mechanism transfers control back to the calling in-
stance when the procedure execution terminates.
As we have already seen, parameters are those features of a procedure which al-
Iowa controlled and well-defined communication, i.e. exchange of information or
data, between the procedure and the outside world. In programming languages we
distinguish between
• actual parameters.
Formal parameters are part of the specification of a procedure, they are local data
objects within a procedure. The specification of formal parameters describes the
way of data exchange in terms of type and input/output. For example,
describes (in MODULA-2 syntax) the two formal parameters a and b to be of type
INTEGER, and to be only input parameters, while the formal parameter c is of type
BOOLEAN and is an input/output parameter.
92 5 Procedures
Actual parameters are those parameters which are actually used when the proce-
dure is called, i.e. they are data objects which the calling and the called instance
have in common. Obviously, they must correspond to the specification of the formal
parameters.
The binding between actual and formal parameters is usually done by the position,
i.e. the k-th actual parameter is bound to the k-th formal parameter. ADA allows
also another way of calling subprograms, where it is possible to state explicitly the
association between formal and actual parameters. For example, consider the fol-
lowing procedure declaration:
P (I, B);
while a call where the association between formal and actual parameters is ex-
plicitly stated could be given as
The example shows that the actual parameters can be given in any order using the
association method.
Several methods are known to pass parameters from a calling instance to a proce-
dure. These methods differ in their effects and can roughly be classified as meth-
ods only for input, or methods for in/out parameters. The methods are explained
using the following principle schema of a procedure declaration and call (Figure
5.2 to Figure 5.5).
VAR a, b INTEGER;
PROCEDURE P (pI, p2: INTEGER);
BEGIN
END P;
P (a, b);
Call by Value
Call by value means that the value of the actual parameter is copied into the stor-
age space of the formal parameter, when calling the procedure (cf Figure 5.2).
Thus, formal parameters can be treated like local variables, i.e. storage allocation
and access is the same as for local variables. Code must be generated to perform
5.2 Parameter Passing 93
the copy operation, which can result in a considerable overhead when passing
large data structures by value. Clearly, that in this case data can be passed into a
procedure, but not out from the procedure.
H±i
copy in
1--- _an n
I-n-b----Hf
Fig. 5.2. Call by value
Call by Reference
Call by reference means that the address of the actual parameter is passed to the
procedure, instead of the value (cf Figure 5.3). Thus, the location of the formal pa-
rameter contains just the address where to find and/or to change the data. So, each
parameter access requires instructions for indirect addressing. Intensive access to
reference parameters in a procedure can therefore cause an overhead. Clearly,
that in this case data can be passed into and out from a procedure.
Call by Value-result
it is copied back when terminating the procedure (cf Figure 5.4). Thus, parameters
can be accessed like local variables. Since there are two copy operations, the
disadvantage of the call by value technique is doubled, but the disadvantage of the
call by reference technique does not occur. Obviously, this technique should only
be applied when passing simple type variables.
I----a----Hii f---~~---~
I----~---r-cp l---~2----~
copy out copy in
copy out
I----a----Hii f---~~---~
I----b----r-cp 1___~2 ~____
copy out
Call by Result
Call by result means that a value is assigned to the formal parameter during the
procedure's execution. This value of the formal parameter is copied to the actual
parameter on termination of the procedure (cf Figure 5.5). Thus, this method is
similar to call by value-result, except that the actual parameter is not copied to the
formal parameter on entering the procedure. Information or data can only be
passed out from a procedure using call by result.
5.2 Parameter Passing 95
Call by Name
Call by name means that the actual parameter substitutes textually the formal pa-
rameter, whenever it occurs. The concept is more or less a historic one. It was
introduced in ALGOL 60 and is not used in recent programming languages.
We quote from the report [NAUR 63]: "Any formal parameter not quoted in the value
list (i.e. call by value parameter) is replaced, throughout the procedure body, by the
corresponding actual parameter, after enclosing this latter in parentheses wherever
syntactically possible. Possible conflicts between identifiers inserted through this
process and other identifiers already present within the procedure body will be
avoided by suitable systematic changes of the formal or local identifiers involved."
procedure P(k);
integer k;
begin
integer if X;
i := i * n; X := k - i;
end;
Now, assuming that we have a nonlocal variable i defined in the block where the
above procedure is defined and called:
begin
integer i;
P (i);
end;
begin
integer i;
j := j * n; X := i - j;
end;
96 5 Procedures
where the procedure call statement is replaced by the procedure body and the for-
mal parameter is textually substituted by the actual parameter. The occurring nam-
ing conflict is solved by renaming the procedures local variable ito j.
Table 5.1 gives several examples on which passing techniques are used by which
programming language.
To conclude this section we compare the results of the different passing methods
by considering the following program fragment:
var k integer;
a array [1 .. 2] of integer;
begin
a [1] .= 1;
a[2] .= 2;
k .= 1 ;
modeTest (a[k]);
writeln('a[l]: ' a[l]);
writeln('a[2]: ' a[2]);
writeln('k k);
modeTest (k);
writeln ('a [1]: ' a [1]);
writeln('a[2]:' a[2]);
writeln ( 'k k) ;
end;
where mode in the formal parameter list of a procedure indicates call by value,
reference, value result, result, or name. Table 5.2 shows the output of the program
for the first three of those parameter passing methods.
In the case of call by result, a syntax error should occur (considering an ADA-like
environment) since the value of x in x : = x * k; is undefined.
Call by name is not included in the table, because it would result in a run-time error
with the first procedure call, i.e. with
k := 3;
98 5 Procedures
ark] := ark] * k;
by which we now have the possibility to plot any function (with one real parameter
and a real result) using the Graph procedure.
According to the original PASCAL report (similar to ALGOL 60 or FORTRAN, for ex-
ample) the number and types of the parameters of the passed procedure must not
be specified, what makes static type checking impossible - especially when using
separate compilation. As a consequence, the ISO and ANSI standards of PASCAL
require that formal procedure parameters must be fully specified with all their own
parameters and in case of a function additionally with its result type. Thus, dynamic
type checking for procedure calls containing formal procedure parameters is no
longer necessary. In MODULA-2 this problem is solved by providing procedure
types as introduced in Chapter 3.4. A MODULA-2 type definition
allows the specification of a procedure type, having one real parameter and pro-
ducing a result of type real. The above given PASCAL example can then be formu-
lated in MODULA-2 as
which means that a procedure of type func is passed to Graph. MODULA-2 allows
only global procedures (Le. those declared in the outermost program block) to be
5.3 Procedure Parameters 99
passed as parameters. This approach allows static type checking of the procedure
(and its parameters) to be passed.
ADA does not allow to pass subprograms as parameters. Instead it provides the
concept of generic program units which is explained in Chapter 5.5.
5.4 Overloading
The term overloading was already introduced in the discussion of elementary op-
erators associated with (simple) data types. Like operators, procedures are said to
be overloaded, if their meaning depends on the type of the arguments (Le. the pa-
rameters). They are useful in situations, where we want to define the same concep-
tual operation on parameters of different types. Subprogram overloading can be
found in ADA, C++, or ALGOL 68, for instance. The following declarations give an
example for subprogram overloading (ADA syntax).
From a compiler writer's point of view the problem of overloaded operators or over-
loaded procedures is the same. The arguments of an overloaded operator deter-
mine the selection of the appropriate instructions, while the parameters of an over-
loaded procedure determine which procedure should actually be called. For ex-
ample, a statement sequence
X := 4.5;
INCREMENT (X, 1.0);
contains a call of the second INCREMENT procedure, while a call of the first
INCREMENT procedure is given by the sequence
Y := 4;
INCREMENT (Y, 1);
Obviously, the type information of parameters (or arguments) must allow an unam-
biguous choice between several possible meanings. A call of an overloaded pro-
cedure which does not exactly match the parameter structure (Le. types and order)
100 5 Procedures
generic
type ELEMENT is private;
type SLIST is array (1 .. 20) of ELEMENT;
procedure GEN SORT (L: in out SLIST) is
begin
end;
showing the generic definition of a sort procedure. The parameter of the sort pro-
cedure is a vector of type ELEMENT. The type ELEMENT is the parameter of the
generic sort procedure which at compile time must be instantiated. This instantia-
tion of the given generic procedure can be done by
to sort integers, or
to sort characters. These two instantiations can be seen as two distinct procedures
(because of different parameter structures) which perform the same algorithm on
different data objects of different types.
In conclusion, the effect of generic procedures is to bind the formal parameters not
at declaration time to a certain type. Although this effect could also be obtained with
a dynamic binding concept (i.e. formal parameters are dynamically bound to actual
parameters at run-time, see e.g. SMALLTALK), the generic concept should be
preferred, since it allows static type checking. Certainly, the generic approach cre-
ates a copy of the code for each instantiation of a generic procedure, while with the
dynamic binding concept only a single copy of the code is necessary.
• Control must be transfered to the called procedure, e.g. loading the ad-
dress of the first instruction of the procedure.
• Control must be transfered back to the calling unit, e.g. restoring the ad-
dress of the instruction that must be returned to on leaving the procedure.
In Section 2.4 activation records have already been introduced as data blocks (not
code blocks) associated with a program unit containing information which is par-
ticular for a specific activation of that program unit. Activation records are used to
manage the above mentioned actions (especially in ALGOL-like languages where
it is possible to have several instances of a procedure's activation record at a time;
it was already pointed out that the static structure of FORTRAN - no recursive pro-
cedures are allowed - can be handled in an easier way).
Dynamic Links
Static Links
Machine Status
Parameters
Local Data
This strategy is exemplified by considering the program segment of Figure 5.7 con-
taining recursively defined procedures. The contents of the run-time stack is traced
in Figure 5.8a to Figure 5.8e. A possible sequence of procedure calls in the
program segment of Figure 5.7 could be P 3 P 1 P 2 P 2.
In Figure 5.8 the activation record of the main program (Dynsto) and the actually
called procedures is shown for each state of the sequence P3 Pl P2 P2. Figure
5.8 e shows clearly that each incarnation of a recursively defined procedure has its
own activation record, and therefore always the most recently defined variables in
procedure P 2 - i.e. those variables, which belong to the actual incarnation of P 2 -
will be accessed.
5.6 Procedure Implementation 103
PROGRAM Dynsto;
VAR x, y: INTEGER;
PROCEDURE PI;
VAR x, y INTEGER;
PROCEDURE P 2 ;
VAR k : INTEGER;
BEGIN
P2;
END;
BEGIN
P2;
END;
PROCEDURE P 3 ;
VAR i INTEGER
BEGIN
PI;
END;
BEGIN
P3;
END.
Fig 5.8a shows the stack containing only the activation record of the program unit
Dynsto.
Figure 5.8b to Figure 5.8e show the stack after each procedure call. If considering
the figures in reverse order one sees the contents of the stack after terminating the
particular procedures. The activation records of the procedures contain the infor-
mation shown in Figure 5.6. The static and dynamic links are explained in the
following.
104 5 Procedures
Transfer of control is not the only thing which must be done, when terminating a
procedure, there is also the stack, which has to be reorganized, i.e. the procedure's
activation record must be released. The removal of an activation record is sup-
ported by the dynamic link, which is the base address of the calling program unit's
5.6 Procedure Implementation 105
activation record within the stack. The chain of dynamic links is referred to as dy-
namic chain. It is called dynamic because it represents the dynamic structure in
which procedures are activated. The dynamic chain of Figure 5.8e is shown in
Figure 5.9, it shows that each activation record has a link to the activation record of
its calling program unit (especially the activation record of the first call of P 2 has a
link to the activation record of Pl, while that of the second call of P2 has a link to the
activation record of the first call of P2).
Since procedures should not only have access to their local variables, but also to
non-local variables of their context (Le all variables of the surrounding blocks), the
actually accessible variables must be defined. This is usually done by using a static
link, which is the base address of the activation record forming the environment of
the procedure. The chain of static links is referred to as static chain. It is called static
because it reflects the static nesting structure of program units or procedures in the
source code. Clearly, the static chain is in general different from the dynamic chain
(especially when considering recursive procedures).
The static chain of Figure 5.8e is shown in Figure 5.10. It shows that the static link
of both, the activation record of P3 and Pl, is the address of the activation record of
the main program Dynsto, because both procedures are declared on the same
level in the declaration part of Dynsto. The static link of both incarnations of P 2
must be the activation record of Pl, since P2 is declared within the block of program
unit Pl. Access to variables of the outermost block from P 2 is then given by the
chain of links from P 2'S activation record to that of P 1 and from this to the activation
record of Dynsto.
106 5 Procedures
A static link of a given program unit references the activation record of the statically
surrounding program unit and, therefore, allows the access to non-local variables.
In case of such an access to non-local variables the compiler generates code to
follow an appropriate number of static links (obviously, the compiler can generate
such a code, since the nesting structure of a source code is static and known at
compile-time). Then, when the correct activation record has been found, the de-
sired variable can be accessed via an offset from the corresponding base address.
This can be very time consuming when the nesting level is accordingly deep.
the static chain. A display will be initialized by copying the calling program unit's
display to the display of the called program unit; additionally the base address of
the calling unit will be entered to the new display. Accessing a non-local variable is
done via an offset from the corresponding base address which is found in the
display. The display can be a part of a program unit's activation record, i.e. it is
stored on the stack. But there are other possibilities to keep the display. If there is
an acceptable number of registers available, then these registers might be used to
maintain the display. It must be mentioned that in this case the maximum nesting
depth of the source code will be limited according to the number of available regis-
ters. Other possibilities to maintain displays are discussed in [FISC 88], for exam-
ple. Applying displays to the situation of Figure 5.8e is shown in Figure 5.11.
Figure 5.12 shows a program fragment with several nested procedures and appro-
priate procedure calls together with a trace of the stack. The program consists of
the procedures pl, p2, p3, and p4; a possible sequence of procedure calls is:
pl p3 p4 p2
the main program calls first procedure pl, pl calls p3, which itself calls p4, and,
finally, p4 calls p2. The contents of the stack after the call of procedure p2 from p4
is shown in Figure 5.12.
The main program is (for sake of simplicity) represented by a very simple activation
record (AR) consisting only of the variable x. The activation records of the
procedures show entries for the corresponding local variables, the appropriate
machine status, as well as the dynamic and static links.
i := j*k - x;
in procedure p2, the involved variables are accessed in the following way:
ii) Variable j is not found in p2'S activation record, thus, we follow the static
link to the activation record of procedure pl, where we find variable j.
iv) Variable x is neither found in p2'S, nor in pl'S activation record. Thus, we
follow the static link to the activation record of the main program, where
we find variable x.
108 5 Procedures
program Links;
var x: integer; dynamic link
procedure pI;
h
static link --
var i, j, k: integer;
procedure p2; status
var i, n: integer;
begin n
AR p2 i
i .= j*k - x;
~
dynamic link
end;
procedure p3;
static link
-'"•
status •
var j, j I: integer; •
•
procedure p4; •
k •
•
var jl, k: integer; •
jl •
begin AR p4 •
•
dynamic link ~
~
p2;
static link '\
•
status •
••
end;
begin •
jl •
•
j ••
p4; AR p3 •
•
dynamic link ~
end; ~
statik link '\
begin
•
status •
p3; ••
k •
••
end; j I
•
I
I
begin i I
AR pI I
X
~
pI; AR Links
end.
In Section 3.5 it was already mentioned, that data and the appropriate operations
should be grouped together (Le. encapsulated), and that implementation details of
both, the data as well as the operations, should be hidden to the users. In this
Chapter we introduce the basic ideas of data encapsulation (and therefore of ab-
stract data types) before we consider certain abstraction techniques in SIMULA 67,
C++, EIFFEL, MODULA-2, and ADA.
Such a procedure abstraction allows to consider the actions that have to be per-
formed for a certain problem. However, a problem's solution means not only to
perform certain actions, but also to consider the problem's data requirements. Data
abstraction - as the second basic and also very powerful abstraction concept - al-
lows us to focus on the data and the operations to be performed on that data with-
out directly providing implementation details, Le. details about the data representa-
tion in memory, as well as details on how the operations should be realized. It rep-
resents the concept of grouping together data types and appropriate operations on
objects of these types as one syntactical unit and by doing this, hiding the imple-
mentation details to those applying these types and operations.
110 6 Data Encapsulation
Parnas described this method as information hiding [PARN 71], [PARN 72]. The ad-
vantages of the method can easily be exemplified by the following: Assume a (very
simple) bank providing the operations: Cashless money transfer; Deposit of cash;
Withdrawal of cash; Checking the balance. A customer can trigger these operations
either at the counter or by mail, without knowing about the bank's internal organi-
zation. For the customer it does not count, whether his money is kept in a certain
drawer, or whether it is kept together with all other money, and whether the links to
the accounts are provided by a simple bookkeeping. Now, assume that customers
know about the internal organization of the bank, and that they have the possibility
of direct access, Le. the self-service of deposit and withdrawal of cash to/from ac-
counts. Then, possible problems are:
Even if assuming that the customers are honest and are working perfect,
the method wouldn't work if something has to be changed in the bank's
internal organization, because an enormous number of customers must
be informed about these changes and the consideration of the changes
must be controlled.
Considering large programming systems or software projects where all data are
public, we will find the same situation, Le. it would be impossible to verify whether
the data is only used in a correct way or whether it is manipulated in an unallowed
and incorrect way. Manipulations such as changes in the representation can cause
severe problems in large software projects where usually a great number of pro-
grammers work together. Thus, the advantage of data encapsulation and informa-
tion hiding as described above is obvious: There is no possibility for improper ma-
nipulations and, if it is necessary to change or to modify the representation of a
6.1 Abstraction, Information Hiding, and Encapsulation 111
data type, this can be done easily in a controlled way affecting only the
(encapsulated) subprograms which manipulate the data type.
The programming language SIMULA 67 was the first language providing encap-
sulation techniques by the class construct, which is the first step towards abstract
data types. More recently other languages such as MODULA-2, ADA, or C++ and
EIFFEL followed, providing all features to define abstract data types.
As already mentioned, the class construct of SIMULA 67 was the first program-
ming language facility allowing encapsulation. Classes in SIMULA 67 are de-
clared - like procedures or other program attributes - in the declaration part of a
block. A class declaration can occur wherever a procedure declaration can occur
(SIMULA 67 is a block oriented language having some similarities to the language
ALGOL 60). The general form of a class declaration is very similar to a procedure
declaration:
CLASS heading ;
parameter specification
BEGIN
declarations
statements
END
where the heading contains the name of the class and formal parameters which
are specified in the parameter specification. The class body represents a
normal block which, therefore, could contain a declaration part (declarations)
with variable, procedure or class declarations, for example.
But it must be noted that the demand for hiding implementation details of data types
and operations on data types is not fulfilled in SIMULA 67. All the attributes of a
class can be accessed from outside similarly to the fields in PASCAL-records.
Thus, local variables are visible to other program units. However, some
SIMULA 67 implementations provide options for making attributes either read-only
or completely hidden, allowing really abstract definitions [HEXT 90].
In the following the example of a queue data type represented by the CLASS QADT
is given. As already mentioned, there are slight differences to the above given
specification, e.g. in the parameter structure of the operators.
6.2 Classes in SIMULA 67 113
INTEGER max;
BEGIN
COMMENT ** definition of the class variables;
COMMENT ** i.e. the queue and related variables;
items := 0;
front := 1;
rear .= max;
END;
Instances of classes are called objects in SIMULA 67 (cf [BIRT 73]). Similarly to the
instantiation of pointer variables in PASCAL, for example, in SIMULA 67 we find
features to define pointer variables (or reference variables in SIMULA 67 termi-
nology) as well as features to instantiate them:
defines the qualified reference variable Q1, i.e. pointer variables are bound to a
certain class, similarly to type binding of pointer variables in other languages (cf
Chapter 3), while
Q1 :- NEW QADT(100);
instantiates a class object, i.e. a new incarnation of the above defined class is gen-
erated with a maximum size of 100 for the queue, appropriate storage is allocated,
and the initialization part of the class is executed. (Note that SIMULA 67 intro-
duces special operators for reference variables, e.g. : - denotes a reference as-
signment, instead of the usual assignment symbol : =.) SIMULA 67 does not pro-
vide features for explicit storage deallocation (e.g. a dispose function), since it pro-
vides an automatic garbage collector to return storage which is no longer used.
6.2 Classes in SIMULA 67 115
The access to class attributes from other program units has the general form
object-reference. attribute-identifier
Thus,
Q1.Insert(e11,si);
inserts element ell to Q1 . queue, if Q1. items < max and the success of the op-
eration is returned in si, while
Q1.IsEmpty;
tests whether Q1 . queue is empty or not. The major problem in standard implemen-
tations is that all local variables are visible to other program units and, therefore,
that those can be manipulated by every other program unit. For example, the legal
assignments
Q1. items .= 0;
or
manipulate the queue in an undesired way, i.e. the first statement actually empties
the queue, while the second puts the front of the queue to an arbitrary position.
Therefore, it is obvious what problems can occur, because the demand for hidden
information for an abstraction is not fulfilled.
It should be mentioned here that classes in SIMULA 67 can not only be defined
and used as shown in the above given simple example, it is also possible to define
class hierarchies. This means that a class can be defined to be a child of another
class, and by this the child inherits the attributes of the parent - a feature which is
important for object-oriented programming languages. The concept of inheritance
is explained in more detail in Chapter 7.
11 6 6 Data Encapsulation
A class in C++ differs from a structure in C in that functions can be class attributes.
Then, those functions can, for example, represent certain operators. The principle
form of a class declaration is given as follows:
class name
members
public:
members
where name specifies a new type name and members represents the declaration
of data, functions, classes, etc. Functions, for example, are then called member
functions, and as they are declared within a class they are explicitly connected to
that class (e.g. to manipulate the data objects of the class). The keyword public
controls the visibility of class members, Le. those class members following the pub-
lic keyword can be used in other program units. But public must not occur in a
class declaration, in such a case all class members are private and can not be
used by other program units.
C++ knows beside classes another form of aggregation: the structure (struct). A
structure is a class with all members public. A private keyword allows then to de-
clare structure members to be private, reverse to the public keyword in classes.
Thus, structures and classes can be used for the same purpose. The only differ-
ence is that structure members are public by default and must be explicitly declared
private, if desired, while class members are private by default and must be explicitly
declared public, if desired.
The following gives an idea on how the above mentioned queue problem can be
solved using C++. C programmers usually "modularize" programming systems by
6.3 Classes in C++ 117
*include <QADT.h>
/* implementation of an abstract data type queue */
/* Author: B. Teufel */
int queue::IsEmpty ()
return items == 0;
Other program units have just to include the file QADT. h for the declaration of
queue variables. For example, consider the following file QADTUse. c
*include <QADT.h>
main
queue ql;
6.3 Classes in C++ 119
queue ql;
if ! ql . IsFull
ql.Insert (ell,si);
it is tested whether the queue represented by ql is full, and if not the element ell
is inserted to the queue ql . q. The success of the insert operation is returned in s i.
Differently to the situation shown for SIMULA 67, sensitive variables, such as
ql. items or ql. front, cannot be accessed in other modules than the implemen-
tation module itself.
C++ provides by the introduced concepts features to define abstract data types. "A
class is a user-defined type" [STRO 86]. The privacy concept together with sepa-
rately compilable program units fulfil the demand of abstract data types for informa-
tion hiding, i.e. the visibility of class members to program units outside the class
can be controlled using the keywords public and private. However, the features
are somehow weak in terms of header files, representing the interface to an ab-
stract data type, and the corresponding implementation file. The problem seems to
be founded on the strong influence of C to C++. A much more elegant and consis-
tent solution was introduced by MODULA-2, as shown in Section 6.5.
an instance of a certain class. A class, say Xl, is used within another class, say X2,
whenever X2 contains the declaration of a variable, i.e. an entity, of type Xl:
e: Xl;
export, listing all features which are available to the clients of the class;
feature, describing the features of the class which are routines (or opera-
tions) and attributes, being data items associated with objects of the
class.
Similar to MODULA-2, only the exported features are available to clients of the
class. Not exported features are said to be secret. The principle form of a class
declaration is given as follows:
where name specifies a new class, i.e. an abstract data type. The feature list repre-
sents secret and non-secret features. The latter are included to the export list which
controls the visibility of the features of the class. The language provides the prede-
fined feature Create to associate an entity with an object. Only after applying the
Create feature to the entity, one can make use of the features defined in the corre-
sponding class. To access a feature, the dot notation is used:
entity-name.feature-name;
rear: INTEGER;
6.4 Classes in EIFFEL 121
within the feature clause is said to be an attribute. The type given in an attribute can
be either INTEGER, REAL, CHARACTER, BOOLEAN, or a class. Constant attributes
are given by a clause like the following:
Constant attributes occupy physical space only once, while attributes occupy
physical space with each appropriate object. According to the language definition
attributes are automatically initialized. For instance, rear in the above given ex-
ample is initialized to zero.
Back to the realization of the given queue problem. Again, we call the class to be
defined QADT and the non-secret features are isempty, is full, insert, and
remove. The definition of the class is given as follows:
feature
items INTEGER;
front INTEGER;
rear INTEGER;
max INTEGER;
queue ARRAY [INTEGER];
Create (qsize:INTEGER) is
create queue providing space for qsize elements
do
if qsize > 0 then
front := 1;
items := 0;
rear := qsize;
max := qsize;
queue.Create(l, qsize)
end; -- if
end; --Create
isempty : BOOLEAN is
-- true if queue is empty, false otherwise
do
122 6 Data Encapsulation
Result := (items 0)
end; -- isempty
isfull : BOOLEAN is
-- true if queue is full, false otherwise
do
Result := (items = max)
end; -- isfull
remove : INTEGER is
-- returns first and removes it from queue,
-- if queue is not empty
require
not isempty
local
f INTEGER
do
f .- front;
front := position (front) ;
item := item - 1;
Result := queue.entry(f)
end; -- remove
For the implementation of the remove and insert procedures we used the pos-
sibility to attribute these procedures by so-called assertions. According to the
EIFFEL terminology assertions are formal properties, such as routine pre- and
6.4 Classes in EIFFEL 123
A class in EIFFEL represents both, the implementation of an abstract data type and
the interface - given by the export list - between this implementation and the clients
of the class. This is different to C++ or MODULA-2, for example, were the interface
to the abstract data type is clearly separated from its implementation.
Since classes are the basic program units in EIFFEL, class QADT will be used
within another class (a client) by declaring an entity of type QADT:
ql: QADT;
Entities must explicitly be instantiated, i.e. associated with an object. This is done
using a Create feature. For example,
ql.Create(lOO);
associates ql with a newly created object, representing a queue of size 100. Once
the instantiation has been done, all the other features of the supplier class QADT
can be applied to ql using the dot notation.
MODULA-2 [WIRT 88a] provides by the module construct a feature allowing the
definition of abstract data types. A MODULA-2 program consists of a program
module, representing the main program, and an arbitrary number of other modules
(called library modules) from which the program module imports entities, such as
variables, types, or procedures. Those entities used from outside must be listed in
an export list of a library module (the definition module, see below) and must be
listed in an import list within the module where they are used. Library modules may
import further entities from several other library modules, which are then also a part
of the program.
Modules in MODULA-2 can be nested, i.e. modules can contain modules (called
local modules) which are hidden from the rest of the program. Local modules are
not important in terms of abstract data types, they just allow to control the visibility of
names within a program. Wirth reports that the rare usage of local modules is the
reason why they are omitted in OBERON [WIRT 88b].
124 6 Data Encapsulation
Library modules are represented by two syntactical units (similar to the interface
and implementation part in C++) which can be separately compiled, provided the
definition module is compiled first:
Differently from C++, definition and implementation modules are a part of the
MODULA-2 language definition; they have to share the same name. Program enti-
ties which are declared in an implementation module and which are not exported
by the corresponding definition module are only visible within this specific imple-
mentation module. The most important difference between abstract data types in
C++ and MODULA-2 is that the module is not a type constructor as the C++ class.
The module is a construct for scope control.
MODULA-2 allows to hide the representation and implementation not only of pro-
cedures and functions, but also of data types by specifying and exporting only the
type's name in the definition module. Such types are called opaque types; their
representation is given in the corresponding implementation module and is not
visible outside that module. A slight restriction is that opaque types must be pointer
types (according to the 3rd edition of Wirth's book "Programming in MODULA-2'),
but this is not a serious drawback since a pointer can point to any data type. This
restriction is a result of separate compilation of definition and implementation
modules. Consider the following situation:
When compiling the program module B, the compiler must allocate storage for vari-
able z and, therefore, has to know the size of variables of type T. Since the specifi-
6.5 Abstract Data Types in MODULA-2 125
The advantage of the realization of opaque types as pointers is that changes in the
representation of a type does not result in the recompilation of modules importing
that type. These information hiding features represent together with the possibility
of separate compilation an excellent feature for encapsulation.
Separate compilation requires that imports must always refer to definition modules.
Therefore, it can be avoided that the recompilation of an implementation module
causes the recompilation of client modules (Le. modules which import entities from
library modules). However, the change and recompilation of a definition module
effects the module's clients.
The realization of the queue problem in MODULA-2 is very simple and can be
given in a very clear way, as shown in the following example. We begin with the
definition module.
TYPE
element INTEGER;
queue; (* a FIFO queue of integer elements *)
(* an opaque type *)
END QADT.
This definition module contains the opaque type queue, which represents a FIFO
queue with elements of type integer. The elements of the queue are assumed to be
integers; the element type is said to be transparent, because its internal structure
(in our example just integer) is visible outside. For more complex structures the
element type has to be replaced. For example, assuming that the queue elements
represent passengers waiting for certain flights of an airline, the elements could be
represented as
element RECORD
Name ARRAY [1 .. 50] OF CHAR;
AirlineCode ARRAY [1 .. 3] OF CHAR;
FlightNo INTEGER;
Class CHAR;
END;
TYPE
queue POINTER TO QueueRec;
QueueRec RECORD
front, rear [1 .. max] ;
items [0 .. max];
el ARRAY [1 .. max] OF element;
END;
BEGIN
(* no initializations are necessary *)
END QADT.
Modules that want to use the queue type and the associated operations must im-
port the type and operations, as in:
MODULE QADTUse;
VAR e1 element;
q1 queue;
si BOOLEAN;
BEGIN
CreateQ (q1);
DeleteQ (q1);
END QADTUse;
6.5 Abstract Data Types in MODULA-2 129
Imported entities are used just by referring the appropriate names. It is not neces-
sary to import all entities which are exported by a certain module, if only a part of
the modules functionality is required. In that case only the desired entities must be
included in the import list. The import list can be abbreviated if all entities of a
module are imported, using the form
IMPORT QADT;
Importing entities from a module in this way has effects on the name qualification,
i.e. imported entities must be qualified explicitly. Considering our example, this
means, for instance, that we have to use
Like in C++, sensitive variables such as q items, etc. cannot be accessed (and,
A •
thus, cannot be manipulated) by other modules than the implementation module it-
self. Opaque types are the only problem with abstract data types in MODULA-2. As
already mentioned an opaque type must be a pointer in MODULA-2. This means
that with the declaration of a variable of an opaque type only a pointer variable is
defined. Therefore, an additional operation to create (or delete) the actual data
structure is needed (in our example CreateQ). Now, the unsolved problem is how
to be sure that the user of such a type actually creates an object of the type using
the provided create function.
The general concept for abstract data types in ADA is very similar to that of
MODULA-2 discussed in the previous section. There are, of course, several syn-
tactical differences, as well as a semantic one. As in MODULA-2 the modularization
of complex problems into program units and the organization of such program units
are considered to be the central issues in the construction of ADA programs.
ADA provides with the package construct a feature allowing the definition of ab-
stract data types. The counterpart of the MODULA-2 definition module is in ADA the
visible part of a package, while the implementation module has its counterpart in
the package body. For example,
shows a package COMPLEX_NUMBERS with its visible part and its implementation
body. As in MODULA-2, both packages share the same name.
As in other languages the visible definition of a type can cause security problems,
because users can make use of the visible information, i.e. the structure of the rep-
resentation of a type. For example, one can manipulate directly the real part or
imaginary part of a data object of type COMPLEX, instead of using the provided op-
erators. In MODULA-2 the integrity of an abstraction is guaranteed by opaque
types. The private concept ensures in ADA the integrity of an abstraction. The pri-
vate concept means, the representation of data objects of an abstract data type is
invisible to other program units, if the type is declared to be private. Now, the major
difference between MODULA-2 and ADA (in terms of abstract data types) is that
MODULA-2, as explained above, requires that opaque types are pointer types,
while private types in ADA are not restricted. The explained size problems occur-
ring by opaque types and separate compilation are solved in ADA by declaring a
private type twice. For example,
package COMPEX_NUMBERS is
-- visible information
type COMPLEX is private
private
invisible information
type COMPLEX is
record
re, im REAL;
end record;
end;
The part of the package specification before the reserved word pri va te is the vis-
ible part which shows the information that is accessible by other program units. The
type COMPLEX is declared to be private and, therefore, implementation details are
not accessible outside the package. After the reserved word private the details of
the type COMPLEX are specified. When compiling a program unit which imports a
private type from another package, the compiler gets enough information about the
private type to determine its size.
6.6 Abstract Data Types in ADA 131
The realization of the queue problem in ADA is very similar to the MODULA-2 reali-
zation. The package specification is given as follows:
package QADT is
private
type QUEUE is
record
FRONT INTEGER range 1 .. MAX 1;
REAR INTEGER range 1 .. MAX .= MAX;
ITEMS INTEGER range O.. MAX .= 0;
EL array (1 .. MAX) of ELEMENT;
end record;
end;
The above explained concept of private types is clearly exemplified in this package
specification and the difference to MODULA-2 can be seen: there is no procedure
132 6 Data Encapsulation
necessary to create a data object of type QUEUE, since it is not a pointer type. The
necessary initializations (e.g. ITEMS . = 0) are given with the type specification in
the private part of the package.
This example shows us the declaration of an abstract data type with no restrictions:
new types can be declared and named and must not - as in MODULA-2 - be made
visible as pointer types, operations on that types are specified by procedures or
functions, and all implementation details are hidden to other program units. The
corresponding package body is given as follows:
EL := Q.EL(Q.FRONT);
Q.FRONT := (Q.FRONT mod MAX) + 1;
Q.ITEMS := Q.ITEMS - 1;
SUCC .= TRUE;
end if;
end REMOVE;
end QATD;
The usage of the package is shown by the following procedure QADTUse which is
going to be a main program in the usual sense. QADTUse has no parameters and it
can be assumed that it can be called somehow.
with QADT;
use QADT;
procedure QADTUse is
El ELEMENT;
Ql QUEUE;
SI BOOLEAN;
begin
end QADTUse;
The first statement of that fragment shows the dependency to the QADT package.
The with clause represents the import list in ADA. The difference to MODULA-2 is
that the with clause always imports all entities of a package, while the MODULA-2
import list allows a selective import of entities. Without the following use statement
we had to refer to entities of the QADT package using the dot notation and qualifying
the package's name, e.g.
Applying the use clause it is not necessary to have such an explicit qualification.
Thus, omitting the use clause we have to have an explicit qualification for the enti-
ties of a package, similar to the explicit qualification in MODULA-2 when using the
abbreviated import list (i.e. using just IMPORT QADT;).
The given procedure is more or less similar to the above given MODULA-2 pro-
gram QADTUse, except that there is no explicit creation of a queue object, because
134 6 Data Encapsulation
private data types are not restricted to pointer types. This is an advantage over
MODULA-2. But the coin has two sides: The disadvantage is that all client modules
must be recompiled if the representation of a type changes. This can be avoided
using pointer types yielding to the MODULA-2 situation.
Important to note is that ADA provides - similar to generic procedures (cf Chapter
5) - generic packages and, thus, generic abstract data types. For example, the re-
striction of the above given queue type to have only 100 elements can be avoided
by declaring the following generic package which, then, can be instantiated for
another size:
generic
MAX : NONNEGATIVE;
package GEN_QADT is
private
type QUEUE is
record
FRONT INTEGER range 1 .. MAX .= 1;
6.6 Abstract Data Types in ADA 135
end;
The package body is the same as before. We can now create and use a queue of a
particular size by instantiating the generic package as in the following fragment:
The behaviour of QADT50 is the same as for QADT, except that the size of the queue
is instantiated by 50. The use clause has the same semantic as for normally written
packages. Another instantiation, e.g.
allows to declare queues of size 250. Obviously, the generic concept cannot only
be applied to the size of a queue in our example, but also to the element type, for
instance (cf [BARN 89], for example). Thus, ADA allows with this generic concept a
more flexible definition of abstract data types than MODULA-2 or C++, for example.
7 Inheritance
A super-class of beers could be given by drinks in general. The three main sub-
classes of beers are alcoholic, light, and non-alcoholic beers. Each of these sub-
7.1 Basic Ideas 137
classes itself can be divided into other sub-classes. In the example only alcoholic
beers are further sub-divided into four classes. The given hierarchical classification
is based on certain properties, each of which characterizes the corresponding
class. Such properties might be attributes like the proof or alcoholic strength in
case of alcoholic beers, or it might be a form of operation, such as "serve with le-
mon" in case of yeast-free wheat beer.
Beers
~
With Yeast No Yeast
Now, the principle idea of such a class hierarchy is that sub-classes inherit the
properties of the corresponding super-classes. For example, a property of beers
could be the operation serve chilled'. Thus, alcoholic beers inherit 'serve chilled'
ft
and proof is added. Wheat beer inherits all these and may add the property of
"must be consumed within a certain time", and yeast-free wheat beer inherits all
those properties and additionally adds "serve with lemon".
The introduced example implies that each class could best be implemented by ab-
stract data types, but in this case it is highly desirable that the programming lan-
138 7 Inheritance
guage provides features allowing the definition of class hierarchies, and by this to
inherit properties (Le. attributes and operations) to sub-classes. Not every lan-
guage providing features to specify abstract data types also provides inheritance
features. Examples are given in the following Sections for SIMULA 67,
SMALLTALK, and C++.
While the usual type classification of data objects results in non-overlapping sets of
data objects, the introduction of subranges and subtypes allows such an overlap-
ping. For example, 'M' is a member of both sets defined by the subranges ['A' ..'P'),
or ['K' . .'Z'] , as well as a member of the set of all characters. They mainly have been
introduced to allow the compiler to generate guards for assignments, e.g. to control
index ranges in this way. The fundamentals of subtypes can be explained in the fol-
lowing way.
Let V(T) be the set of values bound to type T, and O(T) be the operations bound to
type T. Then, the basic idea of subtypes r corresponding to a given type T (called
the supertype) is given by the following simple relationship:
This means that any data object of subtype r is also an object of the supertype T
and most of all that any operation which can be applied to objects of type T can
also be applied to objects of type r. The principle is that a data object of a subtype
can occur wherever an object of the corresponding supertype can occur. Thus, the
concept of inheritance is obvious: A subtype inherits the operations of the corre-
sponding supertype.
Subrange types were introduced in PASCAL, and can also be found in MODULA-2
and ADA. A typical PASCAL (or MODULA-2) subrange type definition can be given
by
where subrange types are allowed to be defined as subranges of any already de-
fined scalar type by specifying the smallest and largest constant value in the sub-
range. We quote from [JENS 74]: "Semantically, a subrange type is an appropriate
7.2 Subranges and Subtypes 139
substitution for the associated scalar type in al/ definitions. Furthermore, it is the as-
sociated scalar type which determines the validity of al/ operations involving values
of subrange types".
Subtypes in ADA are not really new types, they are used to specify objects by cer-
tain types, but with individual constraints or restrictions on their values. Using ADA
syntax, the above given example looks as follows
Objects belonging to different subtypes of the same type can be mixed in expres-
sions and assignments as long as the corresponding restrictions are fulfilled. As in
PASCAL or MODULA-2 the ADA subtype inherits all the operations of the corre-
sponding supertype.
OBERON does not provide subrange types (cf [WIRT 88b]), but it provides as a new
feature the facility of extended record types, i.e. it allows the specification of new
record types on the basis of existing ones, the so-called base types. Extended
types represent a hierarchic relationship to the corresponding base types. Type
extension is discussed in detail in [WIRT 88d].
T RECORD
a, b CHAR
END
Tl = RECORD (T)
c INTEGER
END
Base types and extended types in OBERON are assignment compatible, that
means that variables of an extended type can be assigned to variables of the cor-
responding base type. For example, assume the following variable declarations:
t T;
tl Tl;
t := tl;
t (Tl)
(considering the above example) and it asserts that t is temporarily of type Tl.
Then
t1 := t(Tl);
is allowed.
T RECORD
x, Y INTEGER
END
T = RECORD
x, Y INTEGER; (* public *)
a, b INTEGER (* private *)
END
Only the type declaration in the definition is visible outside, i.e. client modules can
only access the fields x and y of T, the fields a and b are hidden to clients. The vis-
ible or public part of the type declaration T is called the public projection of the
whole type T, as defined in the corresponding module.
It was already explained that SIMULA 67 provides with its class construct not only
an encapsulation facility, but also the facility of inheritance by supporting hierar-
chies of classes. Thus, this Section is closely related to Section 6.2, where we in-
troduced the basic ideas of SIMULA 67 classes and the way how to declare a
class. Once again we want to point out that all attributes of a class can be accessed
from outside, i.e. local variables are visible to other program units.
We adapt here the introduced queue example, i.e. the CLASS QADT, from Section
6.2. The class declaration is repeated for the sake of completeness and readability,
at least in an abbreviated form:
INTEGER max;
BEGIN
INTEGER ARRAY queue (l:max);
INTEGER front, rear, items;
142 7 Inheritance
END;
END;
BEGIN
END;
BEGIN
END;
END;
The declaration
Ql ;- NEW QADT(lOO);
CLASS A;
BEGIN
INTEGER x;
END;
7.4 Inheritance in SIMULA 67 143
A CLASS B;
BEGIN
INTEGER y;
END;
bl :- NEW B;
BEGIN
BEGIN
INTEGER i;
BOOLEAN found;
found := FALSE;
i := front;
END;
END;
and instantiated by
Ql :- NEW QL(lOO);
where 100 matches the formal parameter of QADT. The object Ql allows access to
the following operations and attributes:
B CLASS C;
BEGIN
INTEGER z;
A<B_C
END;
D---E---F
A CLASS B;
BEGIN END;
B CLASS C;
BEGIN END;
A CLASS D;
BEGIN END;
D CLASS E;
BEGIN END;
E CLASS F;
BEGIN END
Let al be a reference variable for class A. Then, the variables of class A can be ac-
cessed by al, e.g. al. x. The variables of A'S subclasses, however, cannot be ac-
cessed directly, they need further qualification. For this case SIMULA 67 provides
the keyword QUA:
al QUA B.y
allows access to y by a 1. But QUA can also be used when a subclass (B) and the
corresponding superclass (A) contain a variable of the same name. For example,
assume bl to be a reference variable of class B, and that both, class A and class B
declare a variable n. Then,
b1.n
bl QUA A.n
CLASS Cl;
BEGIN
PROCEDURE alpha;
beta;
PROCEDURE beta;
OUTTEXT("beta in Cl");
END;
146 7 Inheritance
Cl CLASS C2;
BEGIN
PROCEDURE beta;
OUTTEXT("beta in C2");
END;
Class Cl consists of two procedures alpha and beta, where alpha calls beta,
and beta consists just of an output procedure call. Class C2 consists only of a pro-
cedure beta. Now, assume the following situation
REF (Cl) X;
REF (C2) y;
X NEW Cl;
y NEW C2;
x.alpha; x.beta;
y.alpha; y.beta;
This means, a programmer has only to learn a few concepts to use the SMALL-
TALK system, but he/she (probably) has to translate the usual (e.g. PASCAL) ter-
minology into SMALLTALK terminology. However, the Xerox Learning Research
Group attached great importance to the user interface of the system, thus, support-
ing a programmer's work by the interface: "Every component accessible to the user
should be able to present itself in a meaningful way for observation and manipula-
tion" [INGA 81]. Thus, the SMALLTALK-80 environment is highly interactive and
provides visual communication. The system provides a great number of objects
performing functions such as storage management, file handling, editing, or com-
piling and debugging.
object,
message,
class,
instance, and
method.
alpha + 10
means, the object alpha receives the message arithmetic addition with the argu-
ment 10. On receiving a message, an object looks if the received message
matches one of the messages to which it can respond. This set of respond mes-
sages is called the object's interface, and it describes the operations which can be
invoked for that object.
The objects which are described by a class are called the instances of the class. All
instances of a class have the same interface, i.e. they can respond to the same
messages. Beside this, instances have a private part represented by so-called in-
stance variables - describing the data structure to be allocated on instantiation -
and the set of operations. Both, instance variables and operations are not directly
available to other objects.
ented system. Nevertheless, we use our queue example to give an idea on how
classes in SMALLTALK can be defined:
methods
isEmpty
i items o
isFull
i items max
insert: anElement
self isFull
if True: [ i false
if False: [ rear f- (rear rem max) + 1.
waitingLine at: rear put: anElement.
items f- items + 1.
i true ]
initialize: aSize
items f- O.
front f- 1.
max f- aSize.
rear f- max.
waitingLine f- Array new: (aSize + 1)
new: aSize
i self new initialize: aSize
The method new: is used to instantiate a Queue: self new creates an uninitial-
ized instance which receives the message ini tiali ze with the argument
aSize; the i returns explicitly the initialized instance. The messages at: and put: in
represent
waitingLine[rear] anElement;
150 7 Inheritance
A subclass can add instance variables and methods to the inherited ones. While
the names of added instance variables must differ from the inherited ones, the
names of methods can be the same. In this case, a method of a superclass is over-
ridden by a method of the subclass having the same name. However, the usage of
the pseudo-variable super allows the overridden method to be used, i.e. super in-
vokes the appropriate method from the superclass for a received message. (A
pseudo-variable is available in every method. It must not be explicitly declared, and
its value cannot be changed by assignment. Important pseudo-variable names are
nil, true, and false.)
class name Cl
i.nstance val"i.able names
metfaoC;£S
alpha
self beta
beta
printString 'beta in Cl'
7.5 Inheritance in SMALLTALK-80 151
c[a.ss name C2
superc[a.ss C1
i.nstance vari.a&le names
metholis
beta
printString 'beta in C2'
Now, assume that object x is an instantiation of class Cl, and object y is an instan-
tiation of class C2. Sending message alpha to x invokes method alpha of class Cl
which sends message beta to self. Class Cl can respond to message beta by
invoking method beta. Thus, the output is 'beta in Cl', which is the same when
sending message beta to x. Sending message beta to y invokes method beta in
class C2 and, therefore, the output is 'beta in C2'. Sending message alpha to y
is again the interesting point. Class C2 cannot respond to message alpha, thus,
alpha is sought in the superclass Cl. Method alpha of class Cl is invoked, and
alpha sends message beta to self, which is now y. Thus, it is verified whether
class C2 can respond to message beta. It can, since class C2 contains a method
with name beta. This means, method beta of class C2 is invoked and the output is
'beta in C2'.
One major aspect of inheritance is to have code only at a single place in the system
and, thus, reusing that code. Therefore, it is of great importance for a programmer
to know exactly what code can be find in the class hierarchy. This is probably the
greatest problem with the SMALLTALK-80 system - to know about all the code in
the class hierarchy, which is provided by the system.
The inheritance concept in SMALLTALK is also discussed in [SETH 89] where dif-
ferent examples are considered.
As for SIMULA 67, classes in C++ represent not only an encapsulation facility, but
also an inheritance facility. Again, this means that this Section is closely related to
Chapter 6 where we introduced the basic ideas of C++ classes, as well as the ba-
sic syntactical structures needed to declare and use them.
class name
members
pUblic:
members
where public members are visible outside, and others (the private ones) are not.
Now the principle form of a derived class is as follows:
An important question is now the visibility of inherited members. The default of the
above given construct is that members inherited from the base class are private to
the derived class, no matter whether they have been public or not in the base class.
This means, that the members of the derived class can use the inherited members,
but the inherited members are not accessible to users of the derived class. If this is
desired, the base class has to be declared public:
This means, that a public member of the base class is also a public member of the
derived class and, thus, can be accessed by the users of the derived class. Of
course, private members of a base class remain private in the derived class, they
cannot be made public in the derived class.
We conclude this Section by considering again the example of the two classes C1
and C2, which is given as a C++ fragment as follows:
class Cl
public:
void alpha ();
void beta ();
};
void Cl::alpha () {
this -> beta();
};
7.6 Inheritance in C++ 153
class C2 public Cl
public:
void beta ();
};
main ()
Cl x;
C2 y;
x.alpha(); x.beta();
y .alpha () ; y .beta () ;
With x. alpha () we invoke function alpha of class Cl, which itself invokes function
bet a of class C 1. The result is the same as with x. bet a: the output is "beta in
Cl". With y. beta () we invoke function beta of class C2, and the output is "beta
in C2". Again, y. alpha () is the interesting point. y. alpha () invokes alpha of
Cl, which was bound to beta of Cl at compile time. Therefore, alpha invokes beta
of Cl, and the output is "beta in Cl". This is quite different to SMALLTALK-80,
and we see that inheritance works different in different languages.
class Cl
public:
void alpha ();
virtual void beta ();
};
This means, that whenever possible beta () of the derived class will be invoked
and, thus, the output for our example would be "beta in C2".
8 Concurrency
As already mentioned in Chapter 4, we call any program unit that can be in concur-
rent execution with other program units a task. In Chapter 4 we introduced corou-
tines and tasks in terms of unit control structures. The purpose of this Chapter is to
have a closer look at concurrent program units, the basic ideas underlaying con-
currency, and features in programming languages which are necessary to support
concurrency. An overview of the developments in concurrency and communication
as they took place during the last years is given in [HOAR 90].
When talking about the execution of programs on a computer system, we can dis-
tinguish between the two fundamental concepts
concurrent execution.
Sequential execution means that one statement after the other is executed and,
thus, at any given time only one statement is in execution. In contrast to this, con-
currency stands for parallel execution - or at least the possibility of parallelism. We
call statement sequences, that can be executed in parallel, processes.
8.1 Basic Ideas 155
Obviously, things can only be done in parallel on a machine, if there are facilities
available allowing parallelism. Real parallelism cannot be achieved without an ap-
propriate hardware, Le. without a computer system consisting of several proces-
sors. In this case we talk about a multiprocessor system providing physical paral-
lelism. The operating systems of today's single processor computer systems usual-
ly allow a form of quasi-parallelism which is called multiprogramming. This means,
that on such a system only one process is active at a time, but each process gets
access to the processor for a certain time slice, and there exists a sophisticated
scheduling algorithm to manage the demands of the different processes. For a
more detailed discussion on such operating system facilities see [SILB 88], for ex-
ample.
Now, the important issue for our discussion is that concurrency in programming
languages and parallelism in computer systems are two different things.
Concurrency in a programming language can be understood as parallelism on a
logical level, Le. the programming language provides facilities to specify which
parts of a program (or a system in general) could be executed in parallel.
Parallelism in computer systems stands for multiprocessor systems, or at least mul-
tiprogramming systems. So far it is not important for our considerations, whether
the underlaying hardware system actually allows physical parallelism, or just a
form of quasi-parallelism. We are interested in the features, which programming
languages must provide to allow concurrency.
But why do we need concurrency at all? Because various real life problems are
existing, which can (or must) be modelled by concurrent program units. For exam-
ple, every system conSisting of a process (or several processes) producing some
entities which are consumed by another process (or processes) can be modelled in
a natural way using concurrent units. In general, the requirement for concurrency
can be found in
Processes or concurrent units are typically not totally independent of each other,
rather they interact in certain ways. The two forms of interaction are
communication, and
synchronization.
o
nicate for this reason.
shared variables
/'
C-J process A
"C J process B
c J+-.--~C. . . _J. . .
process A process B
Those problems are subject of the following Sections. But we start with an example
of quasi-parallelism: Coroutines in SIMULA 67.
Recall that coroutines allow the simulation of interleaved processes. The above
mentioned simulations of producer-consumer paradigms can be handled using
coroutines (e.g. the simulation of a single server system, as shown in Chapter 4,
where the queue process is the producer and the server process represents the
consumer). Coroutines are a form of procedures allowing the mutual activation of
each other in an explicit way. The activation of a coroutine A by a coroutine B
means that the execution of coroutine B is suspended, while the execution of
coroutine A is resumed at that point where it has been suspended last. The situa-
tion is the same as for procedures: At any given time there is only one coroutine in
execution.
Coroutines in SIMULA 67 are realized by classes and the primitives RESUME and
DETACH. The general form of a coroutine in SIMULA 67 could be given as follows:
CLASS P;
BEGIN
initialization statements
DETACH;
WHILE TRUE DO
BEGIN
process statements
RESUME (. .. );
END;
END;
pI :- NEW P;
On executing this statement a new class object is instantiated, i.e. a new incarna-
tion of class P is created and the class body is started to be executed. The execu-
tion of DETACH suspends the execution of the coroutine and control is returned to
that unit (the master) which created the incarnation of the class (i.e. execution is
continued with the statement next to pI : - NEW p;). Thus, DETACH is normally
used to allow some initializations before starting the actual simulation.
158 8 Concurrency
The semantics of the RESUME primitive can best be compared with a procedure call
mechanism. RESUME (x) transfers control to coroutine x. The difference to a proce-
dure call is that execution does not start with the first statement of the class body,
but with that statement following the statement which was last executed (e.g. with
WHILE TRUE DO ... if DETACH was the last executed statement).
Thus, SIMULA 67 provides explicit control mechanisms with the primitives RESUME
and DETACH, and the programmer has to decide when control should be transfered
to another coroutine. This means, we have an explicit form of mutual activation, and
these primitives are used for synchronization. Communication between coroutines
can take place either by using shared variables (Le. global variables) or by refer-
encing variables of other coroutines.
BEGIN
BOOLEAN over;
COMMENT ** controls the life of the processes;
INTEGER number;
COMMENT ** the shared variable;
CLASS prod;
BEGIN
REF (cons) cref;
INTEGER u;
u := ININT;
DETACH;
WHILE NOT over DO
BEGIN
number := RANDINT(1,50,u);
RESUME(cref);
END;
END;
8.2 Coroutines in SIMULA 67 159
CLASS cons;
BEGIN
REF (prod) pref;
INTEGER i, j;
i := ININT;
DETACH;
WHILE NOT over DO
BEGIN
j := number;
IF i = j THEN
ELSE
RESUME (pref) ;
END;
END;
over := FALSE;
pl : - NEW prod;
cl :- NEW cons;
pl.cref :- cl;
cl.pref :-pl;
RESUME (pl) ;
END;
The produced entities, i.e. the random integer numbers, of coroutine pl are trans-
fered to coroutine cl using the global variable number. The initialization parts of
both coroutines (given before the DETACH statement) just read in integer numbers
to initialize the variables u and i, respectively.
The master unit instantiates the reference variables pl and cl, i.e. it creates the
corresponding coroutines. The reference variable of the consumer and producer
process is connected to the incarnation of each other's process, respectively. Then,
the master unit starts the producer-consumer process with RESUME (pl) . Thus, it is
ensured that the shared variable number contains a value before this value is con-
160 8 Concurrency
sumed by cl. The usage of the RESUME statements describes the synchronization
of the two coroutines.
We can assume that the control variable over is set in the consumer coroutine.
When it is set to true, the execution of the coroutine instance terminates and control
is transfered back to the program unit that activated the coroutines, i.e. the master
units.
8.3 Semaphores
Thus, the problems are imaginable when two processes compete for the same en-
tities. We need to have some form of synchronization when accessing shared data
structures, i.e. we must avoid that a process has access to a shared variable or
data structure, while another process is manipulating that structure. Dijkstra de-
scribes this situation as follows [DIJK 68b]: n ••• the indivisible accesses to common
variables are always 'one-way information traffic': an individual process can either
assign a new value or inspect a current value. Such an inspection itself, however,
8.3 Semaphores 161
leaves no trace for the other processes, and the consequence is that, when a pro-
cess wants to react to the current value of a common variable, that variable's value
may have been changed by the other processes between the moment of its in-
spection and the following effectuation of the reaction to it."
Some terminology:
Fair scheduling ensures that every process which is waiting (Le. execut-
ing the P-operation) will enter the critical section in finite time, Le. is not
delayed forever (provided that any process, executing a critical section
terminates this section in finite time, of course).
Now, the most interesting question is how to provide mutual exclusion of a critical
section using semaphores. The answer is quite simple and in principle given by the
following fragment, where we suppose to have to processes (pI and p2) and a
single semaphore variable (s) being used by both processes:
var s SEMAPHORE;
process pI;
begin
(* the noncritical section *)
P (s) ;
162 8 Concurrency
process p2;
begin
(* the noncritical section *)
p (s) ;
(* the critical section *)
VIs) ;
(* the noncritical section *)
end;
This means, mutual exclusion of all critical sections can be ensured using the same
semaphore and encapsulating the critical section by the P-operation and the V-op-
eration. The interpretation of the code fragment is obvious: Whenever a process
tries to enter a critical section, it first executes the P-operation using the semaphore
s. This means the process waits until s is greater zero, indicating that the execution
of the critical section can be allowed (Le. no other process has access to the
shared data structure, for example). Naturally, the semaphore s must be initialized
by 1. An initialization of zero for the semaphore would create a deadlock, since the
zero value for the semaphore indicates that some process has access to a critical
section and all others have to wait until that process finishes the execution of the
critical section. On termination of a critical section a process always executes the V-
operation, Le. s := s + 1, giving a signal to the other processes that one of them can
now execute its own critical section. The overall situation is illustrated in Figure 8.2.
Thoroughly considering the above given description, we recognize that the sema-
phore itself is a shared variable, and that the operations on it are critical sections,
Le. they must be done mutually exclusive. This is achieved by implementing the
operations using low-level test-and-set operations, which indivisibly test and as-
sign a value.
It follows from our introduction that the value range of semaphores is given by 0
and 1 (or false and true). Thus, we find often the term binary semaphores. Sema-
phores that can take any positive value are called general semaphores and are
usually used for resource allocation. Then, the semaphore is not initialized by 1,
but by the number of units of a certain resource (e.g. the number of printers). The
semantic of general semaphores is the same as for binary semaphores. A value of
zero means that all resources are connected to a process and that processes de-
manding that resource must wait. Any positive number between zero and the initial
value of the semaphore specifies the number of available resources. Those prob-
lems are of interest in operating systems, for instance.
8.3 Semaphores 163
shared variables
P (s) v (s)
ALGOL 68 provides the type serna to declare semaphore variables, and the func-
tions up and down representing the V-operation and P-operation, respectively.
Concurrent program units in ALGOL 68 are specified as sequences of statements
enclosed by begin and end, and separated by a comma. Statements or units
which are separated by a comma are called collateral statements, and can be exe-
cuted in parallel.
8.4 Monitors
Shared variables and operations on them are grouped together and en-
capsulated in a unit called monitor. The shared variables or data struc-
tures can only be accessed by the procedures which are provided by the
monitor, and these procedures provide mutual exclusive access by
scheduling the processes.
164 8 Concurrency
shared variables
Monitor
Like abstract data types for the manipulation of variables, monitors are an elegant
and clearly structured way to provide synchronization for the access of shared vari-
ables. In both cases, the major actions are performed at one place in a system.
Processes that want access to shared variables use the "exported" procedures,
and their synchronization is done by the monitor itself using the semaphore con-
cept (cf Figure 8.3). Contrary, the usage of semaphores in each process that has
access to a shared variable means that every process has to use the P- and V-
operations as brackets. Especially when considering a great number of processes,
the synchronization using semaphores seems to be an unmethodological and
chaotic approach with a high probability for errors, since synchronization is not
concentrated to one place.
The monitor concept originates in works of Dijkstra, Brinch Hansen and Hoare
[BRIN 73], [HOAR 74]. The first programming language providing the monitor con-
cept was CONCURRENT PASCAL by Per Brinch Hansen. CONCURRENT
PASCAL is "an abstract programming language for structured programming of
computer operating systems. It extends the sequential programming language
PASCAL with concurrent processes, monitors, and classes' [BRIN 75a]. MESA is
another programming language which also provides monitors [MITC 79].
consists of three kinds of so-called system types: PROCESS, MONITOR, and CLASS.
The general form of declaration is as follows:
where name is the name of a process, monitor, or class, and system type is the
appropriate type name. block stands for declarations and a compound statement,
Le. a sequence of statements encapsulated with a BEGIN END pair, and executed
one at a time from left to right. The block or body of a monitor, for example, can
contain private procedures and public procedures (declared to be public by using
the keyword entry), as well as some initialization code. We quote from [BRIN 75aj:
A monitor type defines a data structure and the operations that can be
performed on it by concurrent processes. These operations can synchro-
nize processes and exchange data among them.
Variables of a system type are called system components and are initialized by an
init statement:
The semantics of such an ini t statement is that space for locally declared vari-
ables is allocated, and the initial statement is executed. Once an in it statement is
executed the parameters and the variables of the system component exist forever.
The standard type queue allows only one process to wait in a single queue at a
time. But this is not a major restriction, since a multiprocess queue can simply be
defined as an array of single process queues:
type FB monitor
var buffer array (.1 .. max.) of integer;
items integer;
front integer;
rear integer;
send queue;
rec queue;
buf.remove(el);
end
end;
buf.insert(el);
end
end;
var p Prod;
c Cons;
b FB;
begin
init b, p(b), c(b)
end;
The synchronization of the producer and consumer process is done by the monitor
using the queues send and rec. The producer process is delayed, if the buffer is
full, i.e. an overflow of the buffer is avoided by executing delay (send) , and it is re-
sumed by the consumer process executing continue (send). In the same wayan
underflow of the buffer is avoided by executing de lay (rec) , i.e. the consumer
process is delayed. The consumer process is resumed by the producer by execut-
ing continue (rec). The declared system components p, c, and b are instantiated
by the init statement, which creates the shared data structure and starts the exe-
cution of the producer and consumer processes.
168 8 Concurrency
8.5 Messages
Semaphores and monitors describe good approaches to synchronize communica-
tion between concurrent processes on the basis of shared data structures. There-
fore, both methods are good solutions for quasi-parallel implementations on multi-
programmed single-processor systems. Here, we actually do find the situation of
shared memory.
Like monitors, the message passing model is based on research done by Brinch
Hansen [BRIN 78] and Hoare [HOAR 78). Both provide a language concept for the
communication of concurrent processes without having shared variables, and both
apply Dijkstra's ideas of guarded commands [DIJK 75) to handle the nondetermin-
istic nature of concurrent processes. Brinch Hansen's concept for concurrent pro-
gramming is called distributed processes, while that of Hoare is called communi-
cating sequential processes (CSP).
Calling procedure R means that the values of expr are assigned to the formal pa-
rameters of R, describing the input. On termination of R the output is assigned to
var. Thus, the communication between the processes p and Q become obvious: p
sends certain values to Q as input parameters for procedure R, and Q returns the
output values of procedure R to P. This procedural communication concept is re-
flected in the fact that the calling process has to wait until the called procedure
terminates, Le. " ... the process is idle until the other process has completed the op-
eration (Le. the procedure) requested by it" [BRIN 78].
guard ~ statement-list
if B1 : 81 B2 : 82 ... end
where the first causes a process to wait until one of the conditions becomes true,
and, then, to execute the appropriate statement. The cycle statement just stands
for an endless repetition of a when statement.
Hoare introduced a slightly different form for the communication between two pro-
cesses. Communication between two processes in CSP is specified by input and
output commands, the syntax of which is as follows:
170 8 Concurrency
P ? a
Q ! b
According to Hoare [HOAR 78], input and output commands correspond when an
input command in process A specifies as its source process B, and an output com-
mand in process B specifies as its destination process A, and when the target vari-
able of A's input command matches the value denoted by the expression of B's
output command. In this case communication between A and B is possible. As in
Brinch Hansen's distributed processes, nondeterminism is controlled in esp using
Dijkstra's guarded commands (with a slight change of notation).
A simple example (the classical bounded buffer example) should give an overview
of esp. For this reason, we consider again a producer-consumer process, com-
municating via a buffer-process, which manages a buffer (buf) of size 10. The pro-
ducer process produces elements for the buffer, while the consumer process con-
sumes the elements from the buffer. The buffer-process synchronizes the producer
and consumer process and is given as follows:
buffer: :
buf: (1 .. 10) element;
items := items + 1;
rear := (rear mod 10) + 1;
items> 0; consumer? morel) ~
consumer! buf(front);
front .= (front mod 10) + 1;
items := items - 1;
producer? buf(rear)
buffer ! el
consumer? morel)
consumer! buf(front)
buffer more ()
buffer ? el
and are separated by the guard separator II. Important is that guards can consist of
boolean conditions and/or input commands. A command list which is connected to
a guard can only be executed if the guard does not fail, i.e. if the boolean expres-
sion is true, and if the input command is synchronized with appropriate output
command.
172 8 Concurrency
task T is
task specification
describing the interface to other tasks
end;
task body T is
task body
describing the task's dynamic behaviour
end;
As for packages, the task specification and its body share the same name.
ADA's concept of interaction between tasks was strongly influenced by the above
introduced concepts of message passing. In ADA the mechanism allowing the
communication between tasks is called rendezvous, which means nothing else
than in every day life, when two people meet to perform a certain action, and then
continue independently.
A task specification describes so-called entry points, showing other tasks where
and in which way it allows communication. An entry is defined and called similarly
to a procedure (especially it can - but must not - have in, in out, and out parame-
ters). An entry's parameters describe the exchange of information between tasks.
Its general form is given as
where the name in the accept clause matches the name in the entry clause . As al-
ready mentioned the parameters describe the communication between the task
calling the entry and that task executing the corresponding accept clause. The pa-
rameter mechanism itself is the same as for procedure calls. Omitting the parame-
ters has the one and only purpose of synchronising two tasks. as it might be neces-
sary in some exceptional cases (such as to give the signal to open a valve in case
of overpressure in a pressure system).
time
task A waits
for task B
task A is supendeo
during the rendezvous
time
task B waits
for task A
task A is supendeo
during the rendezvous
The statement sequence of the accept clause describes the actions to be per-
formed during the rendezvous between two tasks. Obviously, this statement se-
quence can also be the empty set which, again, just indicates a synchronization
between two tasks. The statements of an accept clause can consist of blocks, pro-
cedure calls, entry calls as well as other accept statements. Obviously, further ac-
cept statements can only be given for different entries to keep unambiguous.
As usual, a rendezvous can take place between two tasks, say A and B, only if both
tasks are ready to communicate, i.e. if, for example, A executes an entry call of B
and, if B is ready to execute the corresponding accept statement. If this is not ful-
filled, one of the tasks, either A or B, has to wait for the other task. This situation is
shown in Figure 8.4.
Figure 8.4a shows the situation where A executes an entry call of B while B is still
busy to perform some other actions. This means A has to wait until B reaches its
accept statement (i.e. A is enqueued in a waiting line for B). Figure 8.4b shows the
situation where task B is ready to communicate, i.e. it reached already its accept
statement, but task A is still busy. Thus in this case, B has to wait until A reaches the
corresponding entry call. In any case, the task calling an entry of another task is
suspended during the rendezvous takes place. The termination of the accept
clause indicates that both tasks can continue independently and simultaneously.
An interesting difference to Hoare's communicating sequential processes is that
only the calling task has to know the name of the other task (recall, that both primi-
tives "!", and "?" require a task name).
select
select alternative
or
select alternative
else
statement sequence
end select;
where the select alternative can contain an accept statement. Now nondeterminism
means that if all accept statements have non-empty queues of tasks waiting for a
rendezvous, one is chosen nondeterministically.
Doing this in a select statement, each time the select statement is reached, all the
guards are evaluated and only those accept statements are considered for a ren-
dezvous, where the guards are true.
task BUFFER is
entry INSERT (EL: in ELEMENT) ;
entry REMOVE (EL: out ELEMENT);
end;
begin
loop
select
when ITEMS < 10 =>
accept INSERT (EL: in ELEMENT) do
BUF(REAR) := EL;
end;
REAR := (REAR mod 10) + 1;
ITEMS := ITEMS + 1;
or
when ITEMS > 0 =>
accept REMOVE (EL: out ELEMENT) do
EL .= BUF(FRONT);
end;
FRONT .= (FRONT mod 10) + 1;
ITEMS := ITEMS - 1;
end select;
end loop;
end;
BUFFER.INSERT(EL1);
BUFFER.REMOVE(EL1);
In the given example the guard controls the buffer to avoid an overflow or under-
flow of the buffer, i.e. that the producer process cannot insert an element into a full
buffer, and the consumer process cannot remove an element from an empty buffer.
Tasking in ADA is much more complex than it can be discussed within the scope of
this book. Therefore, for more details we refer to [BARN 89].
9 Semantics
In Chapter one, the basic notations to describe the syntax of a programming lan-
guage have been introduced. Since a programming language is a formal language
with a relatively small number of rules describing its form (the syntax), it is not very
difficult to find a formalism which allows to describe the syntax of the language.
Such a description is given by BNF or syntax diagrams, for example. Describing
the form of language constructs is much easier than describing their meaning - we
are faced with similar problems as when considering one of Picasso's paintings, for
example.
Much research work has been done to find a concise, unambiguous and readable
notation to describe the semantics of a programming language. Unfortunately, as
yet none of the proposals made has become a commonly agreed method to de-
scribe semantics. Therefore, natural language is still this medium which is mainly
used to describe the semantics of a programming language. The problems with
natural language are obvious: Natural language is somehow ambiguous and inex-
act and, thus, is interpreted in different ways by different people (e.g. the compiler
writer and the user of the language).
Anyway, we want to introduce briefly the three major approaches in the description
of semantics in programming languages. These approaches are: Operational se-
mantics, denotational semantics, and axiomatic semantics. The first method intro-
duces a virtual computer to describe the meaning of basic language concepts,
while the others represent a mathematical way of description.
According to the summery given in [GHEZ 82]. the most important benefits of formal
semantics are:
An abstract machine consists of some states and a set of simple instructions. The
machine is defined by specifying how the machine changes its state by executing
each of the instructions (i.e. describing a simple automaton model). Then, the se-
mantics of the considered programming language is defined in terms of this ab-
stract (or virtual) computer.
Important for this approach is that the resulting abstract machine is as simple as
possible. It does not matter whether the defined machine can practically be used or
not. What counts is that different interpretations of its code are not possible be-
9.2 Operational Semantics 179
T 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
N DIGIT, NN }
p NN ~ DIGIT NN DIGIT
DIGIT ~ 0 I 1 2 I 3 I 4 I 5 I 6 I 7 I 8 I 9 . }
S NN }
where T, the set of terminal symbols, describes the alphabet over which non-nega-
tive numbers can be generated; N denotes the set of non-terminals; p denotes the
set of productions; s denotes the start symbol (cf Chapter 2). Thus, a non-negative
number is either a digit, or a non-negative number followed by a digit. A digit is ei-
ther a 0, or a 1, or a 2, and so forth, up to 9.
Usually, the non-negative numbers that can be created by the above given gram-
mar denote non-negative integer objects. Therefore, we have to establish a func-
tion which represents the appropriate semantic values of such non-negative num-
bers, i.e. a semantic valuation function <I> that maps the abstract syntactic rules to
the integer objects which they denote (using the conventional interpretation of such
numbers). The function can be given in the following way:
<1>: PL ~ INTEGER
where the outlined square brackets enclose syntactic operands, i.e. elements of the
vocabulary ( V* = (T u N)* ) of the language PL. It must be noted that, for example,
a 4 between the outlined square brackets is an element of the alphabet which de-
notes the integer 4. This means, we find on the right hand side of the equal signs
abstract integer objects which represent the semantic values of the elements be-
tween the outlined square brackets on the left hand side of the equal signs. <I> de-
scribes a mapping from T*, i.e. the sentences of the language PL, to the integer
objects.
For example, the sentence 4711 of the language denotes a semantic value which
can be calculated using the function <1>:
9.3 Denolalional Semantics 181
WHILE ch IN Digits DO
digit := ORO(ch) - ORO('O');
IF num <= (Maxlnt - digit) DIV 10 THEN
num := num * 10 + digit;
readch (ch);
ELSE
num := 0;
Error (... ) (* skip the remaining digits *)
END;
BND;
Similar to that classical example we can define other semantic valuation functions
representing a mapping from language elements to mathematical or logical objects
which represent their meaning. Obviously, a complete system for the description of
a language, such as ADA for example, can get rather complex. However, it is given
in a clear formal way and it allows not only the definition of the semantics of a pro-
gramming language, but it is also the foundation for proving the correctness of
certain programs written in that language as well as proving correctness of the lan-
guage's implementation. Moreover, semantic valuation functions can directly be
used for the implementation of a language. This is shown in Figure 9.1, which
shows a typical code fragment that can be found in the scanner of various
compilers.
The code fragment of Figure 9.1 directly reflects the above introduced semantic
valuation function ell.
182 9 Semantics
{ P} S {Q
A simple example can be given in the following way, where we assume a and b to
be integers:
This means, that if the value of X is x and the value of Y is y before the initiation of
BEGIN R : = Xi X : = Y; Y : = REND, then the value of X will be y and the value
of Y will be x (Le. the values of x and yare exchanged) on completion of the pro-
gram. The predicates p and Q are called precondition and postcondition, respec-
tively.
Especially the first of the two given examples shows that we can have several pre-
conditions for a given statement and a given postcondition; other valid precondi-
tions for this example are
Each of those precondition guarantees the postcondition for the given assignment
statement, but only the first (Le. { b > 3 }) is that precondition which is the least
restrictive one.
While Hoare introduced only sufficient conditions with his formalism, Dijkstra intro-
duced a more precise form with the least restrictive conditions which are sufficient
and necessary [DIJK 75]. This least restrictive precondition is called weakest pre-
condition, and is written as wp(S, 0). Dijkstra calls the rules that describe wp predi-
cate transformer. Thus, Hoare's notation can be replaced by
{ wp(S, Q) } S { Q }
The calculation of the weakest precondition is not always as obvious and simple as
it might be implied by the above given example. More complex actions, such as
WHILE control structures, for example, cause more problems in describing the
predicate transformer. However, to give the reader the idea on how axiomatic se-
mantics will work, in the following we introduce the predicate transformer for as-
signment statements, which is very simple. The assignment
x := expr
a := 10 + 4*b
{ a > 20 }
10 + 4*b > 20
4*b > 20 - 10 ~
b > 10/4
184 9 Semantics
Because b was in the given example assumed to be an integer, the weakest pre-
condition
b > 3 }
holds.
This means, the weakest precondition for the statement sequence Sl; S2 and the
postcondition Q is given by the weakest precondition of statement Sl and the corre-
sponding postcondition, which is the weakest precondition for statement S2 and the
postcondition Q.
(2) Let the language L be defined as L = { On1 n-1 I n ~ 1 }. E.g., 0, 001, and
00011 are elements of L. Design a context-free grammar G (T, N, P, S) which
generates L and where INI = 1 and IPI = 2.
T: {O, 1 }
N: {A, B}
P: A ~ OBB
B ~ 1A I OA I 0
S: A
Show for a suitable word the different syntax trees (parse trees).
(5) What are the differences between PASCAL's type definition facilities and
abstract data types?
186 Exercises
(7) Explain the pros and cons of representing Boolean values as a single bit in
memory.
(8) Choose two high level programming languages and contrast their definitions
of the array data type.
(9) PASCAL provides file and set types. Describe the data objects of these two
types and the appropriate set of operations.
(12) Arrays and records are both aggregates of data. What are the differences?
On which mathematical concepts are they based?
(14) Why is it impossible to allocate memory statically for recursively defined data
structures?
(19) Given the following SIMULA 67 program fragment. List scope and lifetime of
the declared variables in a table.
BEGIN
REAL X, Y;
INTEGER I, J, K, N;
statements
BEGIN
REAL Xl, N;
INTEGER A, J;
statements
BEGIN
REAL X;
INTEGER A, K, N;
statements
END;
statements
BEGIN
REAL A, B, N;
INTEGER H, I;
statements
END;
statements
END;
statements
END;
(20) Given the following PASCAL program. What is the output? Explain.
begin
if e2 then
writeln ('three')
end
else writeln('four');
end.
(21) Discuss the different parameter passing methods. What are the advantages
and disadvantages?
program prog;
procedure procl;
procedure proc2;
procl;
end;
proc2;
end;
procedure proc3;
procl;
end;
proc3;
end;
Describe each stage in the life of the stack until proc1 is called in procedure
proc2. Show the dynamic and static links after each procedure call.
(25) Discuss abstract data types in terms of abstraction, information hiding and
encapsulation (what do these terms mean?). Which features must be pro-
Exercises 189
(26) Does SIMULA 67 provide abstract data types? If YES, give an example. If
NO, explain why.
(28) Compare the facilities to define abstract data types in MODULA-2 and ADA.
What are the major differences?
Give an example for the definition and usage of an abstract data type in both,
MODULA-2 and ADA.
stmt ~ IF expr THEN stmt I IF expr THEN stmt ELSE stmt I other.
class name A
i.nstance vari.abi:e names
methoc!s
gamma
self delta
delta
printString 'ONE'
class name B
su.perclass A
i.nstance vari.abi:e names
methoc!s
delta
printString 'TWO'
(37) Opaque types in MODULA-2 are restricted to pointer types (according to the
3rd edition of Wirth's book "Programming in MODULA-2").
(39) The message passing concept for concurrent processes is based on Brinch
Hansen's distributed processes (DP) and Hoare's communicating sequential
processes (CSP).
program alpha;
var x: integer;
procedure pI;
var xl, x2, x3 : integer;
procedure p2;
var xl, x4 : integer;
begin
xl := x2 + x;
end;
procedure p3;
var x2, xS : integer;
procedure p4;
var x3, xS : integer;
begin
p2;
end;
begin
p4;
end;
begin
192 Exercises
p3;
end;
begin (*main*)
p1;
end.
Describe each stage in the life of the stack until procedure p2 is called
and executed. Show the dynamic and static links after each procedure
call.
c) Explain the access of the variables of the marked statement in
procedure p2 in terms of dynamic and/or static links.
var i : integer;
a : array [1 .. 2] of integer;
i .= 1;
a[l] := 2;
a[2] := 3;
swap (i, a [i]) ;
writeln(i,' ',a[l],' ',a[2]);
Exercises 193
(44) Discuss and compare the selection and repetition statements of the following
programming languages: FORTRAN, ALGOL-68, PASCAL, C, and
MODULA-2,
(45) Compute the weakest precondition for the following statements and post-
conditions:
a) x := 2 * (y - 1) - 1 { x > 0 )
b) h ,= x + 2 - x * 2 { h > 10 )
c) a ,= 4 * b + 2;
b := a/2 - 5;
{ b < 0 }
References
[ACM 78] 1977 ACM Turing Award, CACM 21, p. 613 (1978).
[ACM 85] 1984 ACM Turing Award, CACM 28, p. 159 (1978).
[ADA 79] Part A: Preliminary ADA Reference Manual. Part B: Rationale for the
Design of the ADA Programming Language. ACM SIGPLAN
Notices 14 (1979).
[BACK 57] J. W. Backus, et al. The Fortran Automatic Coding System. Proc.
Western Joint Computer Conference, Los Angeles (1957), 188 -
198. Reprinted in S. Rosen. Programming Systems and Lan-
guages. McGraw Hill, New York (1967).
[BACK 78] J. Backus. Can Programming Be Liberated from the von Neumann
Style? A Functional Style and Its Algebra of Programs. ACM Turing
Award Lecture, CACM 21, 613 - 641 (1978).
[BOWL 79] K. L. Bowles. Beginner's Manual for the UCSD PASCAL System.
Byte Books (McGraw Hill), Peterborough (1979).
[BYRN 91] W. E. Byrne. Software Design Techniques for Large ADA Systems.
Digital Press, Bedford (1991).
[GOLD 83] A. Goldberg, D. Robson. SMALL TALK-80: The Language and its
Implementation. Addison-Wesley, Reading (1983).
[HOAR 81] C. A. R. Hoare. The Emperor's Old Clothes. ACM Turing Award
Lecture, CACM 24, 75 - 83 (1981).
[IEEE 82] IEEE Computer Society. A Proposed Standard for Binary Floating-
point Arithmetic. Draft 10.0, IEEE Task P754 (1982).
[IVER 62] K. E. Iverson. A Programming Language. John Wiley & Sons, New
York (1962).
[JENS 74] K. Jensen, N. Wirth. Pascal User Manual and Report. Springer-
Verlag, Berlin (1974).
[MART 86] J. Martin. Fourth Generation Languages, Vol. I - Vol. III. Prentice
Hall, Englewood Cliffs (1986).
[MEYE 90] B. Meyer. EIFFEL: The Language. Prentice Hall, Englewood Cliffs,
1990.
[NAUR 63] P. Naur (ed.). Revised Report on the Algorithmic Language ALGOL
60. CACM 6 (1963), 1 - 17; Compo J. 5 (1962/63), 349 - 367; Num.
Math. 4 (1963), 420 - 453.
[RICH 69] M. Richards. BCPL: A Tool for Compiler Writing and Systems
Programming. AFIPS Conference Proceedings, 557 - 566 (1969).
[STER 90] L. S. Sterling. (ed.). The Practice of Prolog. MIT Press, Cambridge
(1990).
[WIRT 78] N. Wirth. MODULA-2. Report No. 27, Institut fOr Informatik, ETH
ZOrich (1978).
[WIRT 88d] N. Wirth. Type Extensions. ACM Trans. on Prog. Lang. and Syst.
10,204 - 214 (1988).
[XERO 81] The Xerox Learning Research Group. The SMALLT ALK-80 System.
BYTE 6, 36 - 48 (August 1981).
Index
95; 96; 97; 98; 99; 100; 101; second generation language 3
102; 103; 104; 105; 107 selection statement 81
procedure call 72; 81; 83; 84; 85 semantic analysis 28; 38; 179
procedure parameter 98 semantics 1; 10; 16; 22; 23; 38;
procedure type 59 177; 178; 179; 181; 184
process 154; 155; 156; 157; 161; semaphore 87; 154; 160; 161;
162; 163; 164; 165; 166; 162; 163; 164; 168
168; 169; 170 sentence 16; 17; 18; 20
production 17; 18; 19; 23; 24; 25; separate compilation 5; 8; 98;
27; 30; 31; 32; 33; 34; 36; 124; 125; 130
37; 180 sequential file 60
PROLOG 4; 7; 8 set type 58; 59
public 110; 116; 117; 119; 141; shared data structure 160; 162;
152; 165 166; 167; 168
shared variable 154; 156; 158;
159; 160; 161; 162; 163;
qualified name form 53 164; 165; 168
shift-reduce analysis 33; 35; 36;
37
RAMIS 9
short-circuit evaluation 71
random access file 60
side effect 82
real 44; 47; 48; 49
signal 161
record 53; 54
SIMULA 67 4; 5; 6; 51; 67; 86;
recursion 5; 83
96; 109; 112; 114; 115; 119;
recursive procedure 5; 12; 42; 83;
136; 138; 140; 141; 142;
103; 105
144; 145; 147; 150; 151;
regular grammar 25; 26; 29; 30
154; 157; 158
rendezvous 87; 172; 173; 174;
SMALLTALK 4; 5; 7; 8; 101; 119;
175
136; 138; 145; 146; 147;
REPEAT 77; 79; 80
148; 149; 150; 151; 153
repetition statement 72; 76; 77; 81
SNOBOL 5; 42
RETURN 82
SNOBOL4 55; 57
recursive procedures 40
SQL9
rightmost derivation 24; 33; 34; 35
stack 33; 34; 36; 37; 38; 40; 41;
Ritchie 7
42; 65; 102; 103; 104; 105;
Robson 147
106; 107; 160
Roussel 8
start symbol 24; 27; 28; 33; 35;
row-major form 51; 52; 64
180
rule-based language 4
static binding 46
run-time environment 40
static chain 105; 106; 107
static link 41; 105; 106; 107
scanner 29; 30; 38 static type checking 55; 63; 98;
scope 66; 67; 68 99; 101
Scott 179 storage allocation techniques 42
208 Index
tag field 54
task 81 ; 84; 86; 87; 154; 172; 173;
174; 175
terminal symbol 17; 18; 20; 22;
23;24;28;31; 180
third generation language 3
token 16
top-down parsing 30; 32
Tower of 8abel1
type 0 grammar 25
type 1 grammar 25
type 2 grammar 25
type 3 grammar 25; 26
type binding 4; 114
type checking 39; 43; 47; 62; 63
type discriminator 54
UCSD PASCAL 9
UNIX7
New by Springer-Verlag
Sachsenplatz 4-6, P.O. Box 89, A-1201Wien . Hei delberger Platz 3 ,D·1000 Berlin 33
175 Fiflh Aven ue, New Yo rk, NY 10010, USA · 37-3, Hongo 3-chome, Bunkyo-ku, Tokyo 1l3, Japan