Principle of Programming Languages.
Principle of Programming Languages.
DCA1209
PRINCIPLE OF PROGRAMMING
Unit: 1 - Introduction to Principle ofLANGUAGES
Programming Languages 1
DCA1209: Principle of Programming Languages
Unit – 1
Introduction to Principle of
Programming Languages
TABLE OF CONTENTS
Fig No /
SL SAQ / Page
Topic Table /
No Activity No
Graph
1 Introduction - -
4
1.1 Objectives - -
2 Programming Language Design - -
Program performance and features of i -
2.1
programming languages 5 - 14
2.2 Development of programming languages - -
3.1 Compilation - -
3.4 Interpretation 8 -
4.1 IDE - - 34 - 36
1. INTRODUCTION
Studying the fundamental concepts of programming languages allows learners to produce clearer and
more optimised code and the flexibility to adjust to new languages quickly. This enhances their
problem-solving and debugging skills. It offers a more complete outlook on software development,
promoting creativity and innovation. Acquiring this expertise is essential for enhancing one's
professional growth and is the basis for more advanced computer science subjects. Knowledge of
different paradigms and linguistic features enhances the ability to make informed choices in software
design and optimisation.
In this unit, learners will study Programming language design, which involves creating languages with
qualities like readability, writability, reliability, and efficiency. Compilation is translating high-level
code into machine code before execution, whereas interpretation involves executing code line by line
without requiring prior translation. Programming environments encompass a variety of tools such as
IDEs, command-line utilities, text editors, and debuggers, all of which aid in the software development
process. These fundamental principles assist developers in selecting suitable tools and programming
languages for specific jobs and producing superior code.
To thoroughly understand the topic, learners must understand the basic principles, engage in hands-
on exercises using both compiled and interpreted languages and investigate different programming
environments. They must also participate in practical projects, use debuggers and profilers, and
interact with discussion boards.
1.1. Objectives
After studying this unit, you should be able to:
• Various programming languages, such as Boolean algebra and logic, are built upon a common
mathematical basis.
• They offer comparable capabilities like arithmetic, logic operations, and text processing.
• They are founded on identical hardware and instruction sets.
• Both of them share the same design objectives: to identify programming languages that are
user-friendly and resource-efficient.
• Programming language designers exchange their design experiences.
Some programming languages show greater similarities, while others display greater differences.
Programming languages can be classified into distinct categories based on their similarities or
paradigms. In programming languages, a paradigm refers to a collection of fundamental rules,
concepts, and techniques that dictate how a computation or algorithm is articulated. The primary
paradigms include imperative, object-oriented, functional, and logic paradigms.
The object-oriented programming paradigm is essentially similar to the imperative paradigm, except
that it organises related variables and processes into classes of objects. Access privileges for variables
and methods in objects can be configured to streamline the interaction between objects. Objects are
fundamental components of programmes that enable language features such as inheritance, class
The functional programming paradigm, also known as the applicative programming paradigm,
represents computation using mathematical functions. Given that computing is often represented
using mathematical functions in various mathematics courses, functional programming is expected
to be comprehensible and straightforward to utilise. Nevertheless, due to the stark contrast between
functional programming and imperative or object-oriented programming, transitioning from the
latter to the former can be challenging for programmers who are accustomed to developing code in
imperative or object-oriented paradigms. In functional programming languages, the key distinction
is in the absence of the concept of memory locations. Each function will accept a set of values as input
(parameters) and generate a solitary output value (function's return value). The resultant value
cannot be kept for future utilisation. It must be utilised either as the ultimate result or promptly as
the parameter value of another function. Functional programming involves the process of defining
functions and structuring the output values of one or more functions as the input parameters of
another function. Common examples of functional programming languages are ML, SML, and
Lisp/Scheme.
The logic programming paradigm, also known as the declarative programming paradigm, represents
computation using logic predicates. A logic programme consists of a collection of facts, rules, and
queries. The execution phase of a logic programme involves systematically comparing a question with
each fact and rule in the provided fact and rule base. If the question is matched, we receive an
affirmative answer to the query. Alternatively, we do not receive a response to the query. Logic
programming involves the process of discovering factual information, establishing rules based on
these facts, and formulating queries to articulate the problems we aim to resolve. Prologue stands as
the sole prominent logic programming language.
It is important to acknowledge that numerous languages are classified under multiple paradigms. As
an example, we may say that C++ is a computer language that follows the principles of object-oriented
programming. Nevertheless, C++ encompasses nearly all the characteristics of C, making it an
imperative programming language as well. C++ can be utilised for writing programmes in the C
programming language. Java is predominantly object-oriented, however it also has certain imperative
characteristics. For example, Java's primitive type variables do not acquire memory from the language
heap like other objects.
The effectiveness of a program, encompassing its dependability, clarity, ease of writing, capacity to be
reused, and efficiency, is mostly influenced by the programmer's approach to designing the algorithm
and choosing the appropriate data structures, along with other implementation specifics. The
features of the programming language are essential for supporting and ensuring that programmers
use the appropriate language mechanisms when developing algorithms and data structures. Table 1.1
shows how the characteristics of a language affect the efficiency of a programme developed in that
language.
The table demonstrates that simplicity, control structures, data types, and data structures
substantially influence all elements of a performance. Readability, reusability, writability, and
reliability are all enhanced by syntax design and the use of abstraction. However, they do not exert an
important impact on the efficiency of the programme. Expressiveness enhances the ability to create
code, but it can compromise the program's reliability. Strong type checking and restricted aliasing
limit the flexibility of creating programmes but are widely regarded as yielding more dependable
programmes. Exception handling is a mechanism that protects the programme from abruptly
terminating when it encounters unforeseen situations or semantic problems.
High
Efficiency Readability Reusability Writability Reliability Performance
Programs
error-handling methods, automated memory management (e.g., garbage collection), and strong
type systems to reduce the occurrence of defects and runtime errors.
Early programming languages were designed to closely resemble machine language due to the
constraints imposed by hardware and compilers. Machine languages are the primary languages of
computers and the initial set of programming languages employed by humans to interact with
computers.
Machine languages are comprised of instructions represented by binary integers, which can be
challenging for humans to memorise. Subsequent to programming language development, the
utilisation of mnemonics emerges as the subsequent phase, enabling the representation of commonly
used bit patterns through specific symbols. Assembly language refers to a machine language that
incorporates advanced mnemonics. An assembly language typically supports basic variables,
branching to a specific address, several addressing modes, and macros that can express multiple
instructions. An assembler is a tool that converts an assembly language programme into a machine
language programme. An assembler's primary task is to convert mnemonic symbols into their
corresponding binary representations, replace variables with register numbers or memory locations,
and determine the destination address for branch instructions based on the labels' positions in the
programme.
The origin of the first high-level programming language can be attributed to Konrad Zuse's Plankalkül
programming system, developed in Germany in 1946. Zuse created the Z1, Z2, Z3, and Z4 computers
throughout the late 1930s and early 1940s. The Z4 machine, located at ETH (Eidgenössisch
Technische Hochschule) in Zürich, was used to construct the Plankalkül system. Zuse utilised this
system to design a programme capable of playing chess.
The inaugural high-level programming language that was practically employed in an electronic
computer system was formulated in 1949. The language was designated as Short Code. There was a
lack of a compiler specifically developed for the language, necessitating the manual compilation of
programmes written in the language into machine code.
Grace Hopper is credited with inventing the compiler, as she built the first widely recognised compiler,
known as A0, in 1951.
Alick E. Glennie developed the initial version of Autocoder, which is considered the first primitive
compiler, in 1952. It converted Autocode programmes, which were written in symbolic statements,
into machine language for the Manchester Mark I computer. Autocode has the capability to process
identifiers consisting of a single letter and perform calculations using basic formulas.
Fortran (FORmula TRANslating) was the inaugural widely adopted programming language created
by John Backus and his colleagues at IBM from 1954 to 1957. Backus was, moreover, the co-designer
of the IBM 704, which operated the inaugural Fortran compiler. Later, Backus participated in creating
the Algol programming language and the Backus-Naur Form (BNF). BNF, short for Backus-Naur Form,
served as a formal notation for precisely specifying the syntax of programming languages. Fortran II
was introduced in 1958. Fortran III was developed in late 1958, but it was not made available to the
general public. Subsequent iterations of Fortran include ASA Fortran 66 (Fortran IV) in 1966, ANSI
Fortran 77 (Fortran V) in 1978, ISO Fortran 90 in 1991, and ISO Fortran 95 in 1997. In contrast to
assembly languages, the initial iterations of Fortran permitted the usage of several variable types
(such as real, integer, and array), supported procedure calls, and incorporated basic control
structures.
Algol 58 was announced in 1958, after the experience gained from Fortran I. Two years later, Algol
60, the initial block-structured language, was introduced. The language underwent revisions in 1963
and 1968. Edsger Dijkstra has been recognised as the creator of the initial Algol 60 compiler. He
gained renown for his leadership in pioneering structured programming and for his efforts to
eliminate the use of the goto statement in programming.
Pascal, a programming language, was created by Niklaus Wirth between 1968 and 1970 and has its
origins in Algol. In 1977, he made significant advancements to Modula, which was intended to replace
Pascal. He subsequently went on to develop Modula-2 in 1980, and Oberon in 1988. The Oberon
language included a syntax similar to Pascal, but it had a powerful typing system. Additionally, it
Dennis Ritchie developed and first implemented the C programming language at DEC from 1969 to
1973. It was designed as a system implementation language for the early Unix operating system. It
quickly emerged as one of the prevailing languages throughout that era and continues to hold that
position today. The antecedents of C were the typeless language BCPL (Basic Combined Programming
Language) developed by Martin Richards in 1967, followed by the creation of B by Ken Thompson in
1969. The C programming language employs a weak type checking system in order to facilitate a
greater degree of programming flexibility.
The Simula language, developed by Ole-Johan Dahl and Kristen Nygaard at the Norwegian
Computing Centre (NCC) from 1962 to 1967, was the first to introduce and implement object-oriented
programming concepts. Simula I was initially developed as a programming language specifically for
simulating discrete events. Yet, Simula 67, its updated iteration, emerged as a comprehensive and
versatile programming language. Despite its limited popularity, Simula had a significant impact on
the development of modern programming paradigms. It presented significant object-oriented
principles such as classes and objects, inheritance, and late binding.
Smalltalk, developed at Xerox PARC under the leadership of Alan Kay, is one of the object-oriented
programming languages that followed Simula. The versions that were created encompassed
Smalltalk-72, Smalltalk-74, Smalltalk-76, and Smalltalk-80. Smalltalk also acquired functional
programming characteristics from Lisp.
C++ was created by Bjarne Stroustrup in 1980 at Bell Labs as an extension of the languages Simula
67 and C. Originally known as "C with classes," it was later improved and renamed as C++ in 1983.
C++ was seen as an improved version of C, with robust type checking and incorporating data
abstraction and object-oriented programming principles acquired from Simula 67.
Java was authored by James Gosling, Patrick Naughton, Chris Warth, Ed Frank, and Mike Sheridan at
Sun Microsystems. The programming language was initially named Oak, but it was later changed to
Java when it was officially announced to the public in 1995. Java's antecedents were C++ and Smalltalk.
Java eliminated the majority of non-object-oriented characteristics from C++ and emerged as a more
streamlined and superior object-oriented programming language. The language's dominance in
creating Internet applications can be attributed to its two-level programme processing idea. This
concept involves compiling the code into an intermediate bytecode and then interpreting the
bytecode using a small virtual machine. Java remained an impure object-oriented programming
language. The primitive kinds, such as integers, floating-point numbers, and Booleans, were not
implemented as classes. Instead, their memory allocations were made from the language stack rather
than the language heap.
The announcement of Microsoft's C# language took place in June 2000. This language is a derivative
of both C++ and Java. The language was implemented in a fully object-oriented manner, without
needing "primitive" types. C# also prioritises component-oriented programming, an enhanced
iteration of object-oriented programming. The concept is to have the capability to construct software
systems by utilising premanufactured components.
Lexical Syntactic
Contextual Semantic
i. Lexical Structure: The lexical organisation of a language determines its vocabulary. Lexical units
are regarded as the fundamental components of programming languages. The lexical structures
of programming languages are typically analogous and typically encompass the following types of
entities:
• Identifiers: Programmer-selected names used to represent objects such as variables, labels,
procedures, and functions. Typically, computer languages mandate that an identifier must
commence with an alphabetic character and may optionally be succeeded by alphanumeric
characters and certain special symbols.
• Keywords: Reserved names designated by the language designer and utilised to construct the
syntactic framework of the language.
• Operators: Symbols Used to denote the operations. Every general-purpose programming
language should include essential operators, such as mathematics operators (+, −, *, /),
relational operators (<, , ==, >, ), and logic operators (AND, OR, NOT, etc.).
• Separators: Symbols employed to delimit lexical or syntactic elements of the language. Space,
commas, colon, semicolon, and brackets serve as separators.
• Literals: Values that can be assigned to variables of various sorts. For instance, integer-type
literals refer to integer values, character-type literals encompass any character from the
language's character set, and string-type literals encompass any sequence of characters.
• Comments: Any descriptive text included within the programme. Comments are initiated by a
designated keyword or separator. The compiler disregards any comments when translating a
programme into machine code.
Contextual structure relates to the interpretation of the program's meaning. A sentence that is
grammatically ((lexically) correct may not be semantically (contextually) correct.
Example:
stringstr = "hello";
int i = 0;
int j = i + str;
In the above example, all declaration statements are syntactically correct. However, the last one
is semantically incorrect because it is not meaningful when combining an integer variable with a
string variable.
3. COMPILATION vs INTERPRETATION
3.1. Compilation:
Definition: Compilation is converting source code written in a high-level programming language into
a lower-level language, like machine code, which can be executed by a computer.
Importance of Compilation: Compiling your code is essential as computers can only understand
machine code. Compiling is the process of converting code that is written in a format that humans can
understand into a format that computers can understand and execute.
How it works: During Compilation, the source code is examined, parsed, and transformed into an
intermediate representation known as object code. Subsequently, the object code is combined with other
essential files to generate an executable program.
Compiler: A compiler is a software instrument that executes the compilation process. The system's
input is the source code, which is then processed to produce the output in the form of an executable
program or object code.
The initial three stages are commonly called the Analysis Phase, and the final three are called the
Synthesis Phase.
i. Lexical Analysis:
Lexical analysis, also known as lexing or scanning, is the first phase of the compilation process in
which the source code is analysed and divided into lexical units known as tokens. The lexical
analysis process examines the input code on a character-by-character basis.
The lexical analyser would produce 7 tokens, stored as 7 records in the Symbol Table.
In this phase, a string named "name" is assigned the value "Oranges". The Symbol table is then
generated and filled with the tokens that were generated. A symbol table is a commonly used data
structure that stores a record for every identifier found in the source code.
This phase is responsible for managing type checking. This guarantees that the variables are
assigned values in accordance with their declaration.
If a variable is declared as an integer and then assigned a float, the Semantic Analyzer will detect
and report the issue.
This stage also detects segments of code, such as operands and operators, within the input code.
An example can be seen in the compilation of Java programmes into Java Bytecodes, which are
stored as .class files and executed by the Java Virtual Machine. This intermediate code is
compatible with any operating system that supports the Java Virtual Machine (JVM).
v. Code Optimisation:
Code optimisation involves removing unnecessary code and optimising for efficient memory
management, resulting in improved execution performance. The intermediate code guarantees
the generation of a target code that can be executed on any system, thus facilitating portability
across several platforms.
Ahead-of-time
Single-pass Multi-pass Just-in-time
compilers
Compilers Compilers Compilers (JIT)
(AOT)
Source-to-
source
compilers
i. Single-pass Compilers:
Single-pass compilers are a specific sort of compiler that processes the source code and produces
machine code in a single iteration without returning to previous code sections. Typically, they
have higher speed than multi-pass compilers, but they may have lower efficiency in terms of code
quality. Single-pass compilers are advantageous for efficiently handling substantial volumes of
code, particularly when the code is uncomplicated and direct, as shown in Figure 2.
Working:
Single-pass compilers are a specific form of compiler that is capable of reading and generating the
target code in just one pass over the input. Unlike multi-pass compilers, single-pass compilers do
not require numerous iterations over the source code in order to produce the target code. Single-
pass compilers execute a predetermined sequence of actions to produce the desired target code:
Initially, the source code undergoes a scanning process where individual tokens are recognised
and saved in a data structure known as a symbol table.
Subsequently, the tokens undergo parsing to produce an abstract syntax tree (AST) that
accurately depicts the code's structure.
The target code is generated by the single-pass compiler as the AST is generated. Typically, this
process involves traversing the Abstract Syntax Tree (AST) and producing the desired code for
every node in the tree. Single-pass compilers can also optimise the target code by eliminating
unnecessary instructions and improving memory utilisation.
Ultimately, the compiler generates the target code and stores it either in a file or directly in
memory. Single-pass compilers are beneficial for programming languages with uncomplicated
syntax and do not require several iterations over the source code to produce the target code
quickly. Nevertheless, they may not be appropriate for languages with intricate syntax and require
several iterations over the source code.
Working:
Multi-pass compilers are a specific kind of compilers that analyse and produce the target code by
making numerous passes through the input. Unlike compilers that only do a single pass, these
compilers require numerous passes over the source code in order to build the target code.
In the initial phase of a multi-pass compiler, the source code is scanned and a symbol table is
constructed. This table contains data on variables, functions, and other symbols utilised in the
programme.
The symbol table is subsequently utilised in later runs to do semantic analysis, which verifies for
mistakes such as type inconsistencies or variables that have not been declared.
During the analysis process, the compiler produces an intermediate representation (IR) of the
programme. This IR is a high-level and abstract representation of the source code.
During succeeding iterations, the multi-pass compiler applies optimisations to the intermediate
representation (IR), including eliminating superfluous instructions, rearranging instructions for
improved efficiency, and optimising memory allocation.
The last stage of the compiler produces the target code by utilising the optimised intermediate
representation (IR). The target code can be in the form of machine code, assembly language, or
any other executable language for the target platform as shown in figure 3.
Just-in-time (JIT) compilers are a specific kind of compiler that generates machine code during
programme execution instead of translating the complete programme beforehand, as shown in
Figure 4. They are frequently employed in virtual machines, such as those utilised for Java
and .NET, to enhance performance by optimising the code as it is being executed.
Working:
Just-in-time compilers function by compiling code during runtime instead of ahead-of-time
compilers that compile code before execution.
During programme execution, the Just-In-Time (JIT) compiler examines the code and creates
machine code that may be directly executed by the processor.
The JIT compiler initially produces a low-level intermediate code that is optimised for runtime
performance.
Subsequently, the code is compiled dynamically into machine code while the program executes.
The created code is kept in the computer's memory and can be utilised again whenever necessary.
The JIT compiler is capable of performing optimisations that are not feasible with ahead-of-time
compilation since it generates code depending on the current execution context.
For example, it can incorporate function calls within the code, enhance the efficiency of loops, and
remove unnecessary code depending on the program's actual behaviour.
Working:
During the initial phase, the compiler scans the source code and conducts a lexical analysis to
recognise the code's fundamental components, including keywords, operators, and identifiers.
The subsequent phase entails parsing the code to generate a parse tree, which depicts the code's
hierarchical structure. Subsequently, the parse tree is employed for semantic analysis, wherein
the code is examined for faults, and the pieces of the code are given significance based on the
context.
After the code has been examined and confirmed, the AOT compiler produces machine code by
employing a code generation method that converts the code into the desired machine language.
The compiler optimises the machine code to enhance performance, size, and efficiency.
The AOT compiler produces an executable file as its result, which may be distributed and run on
the target system without requiring a second compilation process. AOT compilers are frequently
employed in systems characterised by limited resources or strict performance demands, such as
embedded devices or real-time systems, where the program's initialisation time must be
minimised.
v. Cross compilers:
A cross-compiler is a compiler that enables you to create code for one platform and generate
executable code for a different platform. The cross-compiler enables code writing and testing on
the development system prior to installing it on the target hardware.
Working:
Cross-compilers are specifically designed to produce executable code for a platform that differs
from the platform on which the compiler itself is operating.
A cross-compiler is a tool that converts source code written in one programming language into an
executable format suitable for running on a different platform.
Cross-compilers are valuable tools for software development in scenarios involving embedded
devices or platforms with restricted resources.
Developers are able to write code on a high-performance computer and then produce optimised
code for the desired platform without needing extra hardware or development resources.
This enables expedited compilation durations and enhanced utilisation of system resources, as t
he compiler does not necessitate recompiling the entire programme whenever a
modification is implemented.
Working:
• The process of incremental compilation generally consists of multiple sequential steps:
• Dependency analysis is a process in which the compiler examines the relationships between
source files to identify which files require recompilation.
• Incremental compilation refers to the process of recompiling only the updated source files and
any other files that depend on them.
• Linking: The recently compiled code is combined with the preexisting code to generate the
ultimate executable or library.
• Incremental compilers can significantly accelerate the development process and decrease the
time needed for testing and deployment by compiling only the updated code.
Working:
Optimising compilers employ a combination of static and dynamic analysis approaches to detect
sections of the code that can be enhanced.
Static analysis is examining the code without executing it, and dynamic analysis involves profiling
the code at runtime to detect performance bottlenecks.
This could involve reorganising the code to minimise memory access, reducing unnecessary
processes, and implementing other optimisation techniques.
Afterwards, the compiler produces optimised machine code that executes more efficiently and
consumes fewer system resources compared to unoptimized code.
Working:
Debugging compilers are specifically engineered to assist developers in detecting and rectifying
faults in their code.
They function by including supplementary data into the compiled code, so enabling more
efficient debugging.
When a developer builds their code using a debugging compiler, the resulting executable file has
extra metadata that provides information about the source code and the program's internal state.
Debugging tools can utilise this metadata to pinpoint fault locations, trace data flow inside the
program, and examine the program's memory and CPU status.
Debugging compilers may incorporate supplementary code that executes error checking and
validation, such as boundary checks and null pointer checks.
This can help detect mistakes during programme execution and offer more comprehensive error
messages that are more accessible to developers.
Working:
Source-to-source compilers, commonly referred to as transpilers, transform source code from one
computer language into another.
The procedure involves analysing the source code, constructing an abstract syntax tree, executing
code modifications and enhancements, and subsequently producing the corresponding source
code in the desired programming language.
The initial phase involves understanding the source code and transforming it into a methodical
arrangement that can be effectively handled by the compiler.
This process involves carrying out lexical analysis and syntactic analysis on the code to generate
an abstract syntax tree (AST). After the Abstract Syntax Tree (AST) is generated, the compiler
executes a series of transformations and optimisations to generate code that is equivalent to the
target language.
These modifications may involve altering variable names, substituting functions, and reorganising
control flow statements.
Ultimately, the compiler produces the resulting code, which may be produced and run on the
intended platform.
3.4. Interpretation
Machine code conversion is necessary for all high-level languages to ensure that the computer
understands the programme after receiving the necessary inputs. An interpreter is a software that
converts high-level instructions into machine-level language, line by line. It is different from a
compiler or assembler.
The interpreter in the compiler examines the source code sequentially, analysing each line. If an error
is detected on any line, the execution is halted until the error is rectified. The interpreter's ability to
deliver a line-by-line error makes error repair relatively straightforward. However, the programme
requires a longer duration to complete its execution successfully. The use of interpreters was initially
implemented in 1952 to facilitate programming within the constraints of computers during that era.
It converts source code into a highly efficient intermediate representation and executes it without
delay.
Byte-code
Interpreters
Threaded
Self Types of
Code
Interpreters Interpreters
Interpreters
Abstract
Syntax Tree
Interpreters
i. Byte-code Interpreters:
• Bytecode interpreters initially transform source code into bytecode, which is a condensed and
enhanced representation of the source code.
• Bytecode is distinct from machine code, but it is capable of being executed by the interpreter.
• The bytecode interpreters use a combination of a compiler and an interpreter, thereby earning
the name "compreters".
• Every bytecode instruction begins with a single byte, which allows for a maximum of 256
instructions.
• Examples of programming languages include Java, Raku, Python, PHP, Tcl, and more.
• Unlike interpreters that process bytecode, there are no limitations on the quantity of
instructions that can be executed, provided that there is sufficient memory and address space
accessible.
• Examples of programming languages include Forth, Smalltalk, Lisp, Postscript, and others.
message. Ruby also features an interactive shell that enables users to execute code line by line
and receive immediate response.
• JavaScript Interpreter: JavaScript is a widely-used scripting language employed in the field of
web development. An interpreter is utilised to run the code. The JavaScript interpreter parses
JavaScript code and translates it into executable instructions that can be understood by the
machine. The interpreter sequentially executes the code, processing each line individually. In
the event of a coding error, the interpreter halts execution and presents an error message.
JavaScript features a console that enables users to execute code sequentially and receive instant
feedback.
• Perl Interpreter: Perl is a programming language that operates at a high level and is commonly
employed in web development, system management, and network programming. The code is
executed using an interpreter. The Perl interpreter processes Perl code and translates it into
instructions that can be understood by a computer. The interpreter executes the code line by
line, and if there is a problem in the code, it stops execution and shows an error message.
• PHP Interpreter: PHP is a widely used server-side programming language used in building
websites. It employs an interpreter to carry out code execution. The PHP interpreter processes
PHP code and translates it into instructions that can be understood by a computer. The
interpreter sequentially executes the code, processing each line individually. In the event of a
coding error, the interpreter halts execution and presents an error message. PHP features an
interactive shell that enables users to execute code line by line and receive instant responses.
• Lisp Interpreter: Lisp is a collection of computer programming languages used for artificial
intelligence research and programming language research. Lisp employs an interpreter to carry
out code execution. The Lisp interpreter parses Lisp code and translates it into machine-
executable instructions. The interpreter sequentially executes the code, processing each line
individually. In the event of a coding error, the interpreter halts execution and presents an error
message. Lisp also features a REPL (Read-Eval-Print Loop) mode, enabling users to execute
code line by line and receive instant feedback.
• Shell Interpreter: Shell scripts are used to automate recurring processes on a computer
system. Shell scripts use an interpreter to execute code. The shell interpreter processes shell
script code and translates it into instructions that can be understood by the processor. The
interpreter sequentially executes the code, processing each line individually. In the event of a
coding error, the interpreter halts execution and presents an error message.
C, C++, C#, and other programming languages a Python, Ruby, Perl, SNOBOL, MATLAB, andothe
re compiler-based. r programming languages are interpreted.
4. PROGRAMMING ENVIRONMENTS
A programming environment refers to the set of tools utilised for software development.
The key components of Programming Environments are Text Editors, IDEs, Compilers and
Interpreters etc.
4.1. IDE
An Integrated Development Environment (IDE) consolidates essential development tools into an
integrated software environment.
• File system: An Integrated Development Environment (IDE) incorporates a file system that
facilitates the organisation, administration, and traversal of project files and directories.
• Text editor: The text editor is an essential component that allows developers to write and
modify source code.
• Linker: A linker is a software utility that merges multiple built object files into a single
executable or library.
• Compiler: A compiler converts high-level source code into machine code or intermediate code
that can be run by a computer.
• Integrated tools: These tools, such as a debugger and profiler, are included in the IDE to
improve the development workflow.
Step 5: Linking
Link the object files using Compile -> Link or pressing Ctrl + F9.
Step through the code using Debug -> Step Into (F7) or Step Over (F8).
5. SUMMARY
The objective of programming language design is to develop languages that enhance programme
performance and include fundamental attributes such as readability, writability, reliability, and
efficiency. Technological breakthroughs and growing software needs have significantly influenced the
development of programming languages. This process involves organising languages into four
distinct layers: lexical, syntactic, contextual, and semantic. These layers control many elements of
code processing and interpretation, ranging from tokenising input to understanding context and
semantics. Efficient programming language design guarantees that the language is simple to
understand to humans, written with high efficiency, and executed accurately in diverse settings,
hence improving total programme performance.
Compilation and interpretation are two key approaches for executing programmes. Compilation is
the process of translating high-level code into machine code. It involves various phases, namely
preprocessing, compiling, assembly, and linking. This procedure generates an executable file that is
capable of being executed on a designated machine. Different categories of compilers, including cross-
compilers and just-in-time compilers, have distinct functions, such as facilitating code execution on
diverse platforms or enhancing runtime performance. Interpretation, on the other hand, processes
code sequentially during runtime, offering instant feedback and fault identification but typically
resulting in slower execution. The decision to use either a compiler or an interpreter is contingent
upon criteria such as the requirement for fast execution, the ability to detect errors, and the
convenience of the development process. Programming environments, particularly Integrated
Development Environments (IDEs), incorporate these procedures by offering tools such as text
editors, compilers, linkers, and debuggers. Integrated Development Environments (IDEs)
significantly improve the productivity of software development by incorporating advanced
functionalities like syntax highlighting, code completion, version control, and visual programming.
These capabilities are essential in contemporary programming processes, making IDEs indispensable
tools. Notable instances of Integrated Development Environments (IDEs) encompass Microsoft Visual
Studio, NetBeans, Turbo C, Code::Blocks, Dreamweaver, and Arduino. Each of these IDEs serves
distinct programming requirements and supports various programming languages.
6. SELF-ASSESSMENT QUESTIONS
Multiple choice Questions
1 Which of the following is NOT a feature of programming languages?
A) Readability
B) Write-only
C) Reliability
D) Efficiency
2 What is the primary goal of programming language design?
A) Code obfuscation
B) Program performance
C) Increasing bugs
D) Decreasing readability
3 Which type of compiler translates source code into machine code for a different platform
than the one it is running on?
A) Cross-compiler
B) Just-in-time compiler
C) Native compiler
D) Optimizing compiler
4 Which programming environment provides tools like text editors, compilers, and debuggers?
A) IDE
B) CLI
C) GUI
D) API
5 What is the primary purpose of an Integrated Development Environment (IDE)?
A) Playing games
B) Enhancing developer productivity
C) Listening to music
D) Sending emails
6 Which programming language design goal emphasizes the ease of understanding code?
A) Writability
B) Reliability
C) Readability
D) Efficiency
7 What does the lexical analysis stage of compilation involve?
A) Converting high-level code into machine code
B) Breaking down source code into tokens
C) Optimizing the generated machine code
D) Combining object files into an executable
True or False:
7. GLOSSARY
Financial Management is concerned with the procurement of the least cost funds, and its effective
8. TERMINAL QUESTIONS
1. Explain the program performance and features of Programming languages.
2. Elucidate the development of programming languages.
3. Explicate the structures of programming languages.
4. Briefly explain the stages of compilation.
5. Elucidate Single-pass compiler and multi-pass compiler in detail.
6. Differentiate between JIT and AOT.
7. Define Interpreter. Explain its types.
8. Differentiate between Compiler and Interpreter.
9. Write a short note on IDE.
10. Explicate the features and tools in IDE.
9. ANSWERS
Self-Assessment Questions
1. Write-only
2. Program performance
3. Cross-compiler
4. IDE
5. Enhancing developer productivity
6. Readability
7. Breaking down source code into tokens
8. True
9. False
10. False
10. REFERENCES
1. Maurizio Gabbrielli, Simone Martini, “Programming Languages: Principles and Paradigms”, 2010.
DOI 10.1007/978-1-84882-914-5
2. https://he.kendallhunt.com/sites/default/files/uploadedFiles/Kendall_Hunt/Content/Higher_E
ducation/Uploads/ChenTsai_ProgramLanguages_4e_Chapter1.pdf
3. Mike Grant, Zachary Palmer, Scott Smith, “Principles of Programming Languages”, 2024.
4. Software Engineering by Anuradha A. Puntambekar | PDF (scribd.com)
DCA1209
PRINCIPLE OF PROGRAMMING
LANGUAGES
Unit: 2 - Language Spectrum and Study Motivation 1
DCA1209: Principle of Programming Languages
Unit – 2
Language Spectrum and Study
Motivation
TABLE OF CONTENTS
Fig No /
SL SAQ / Page
Topic Table /
No Activity No
Graph
1 Introduction - -
4-5
1.1 Objectives - -
2 Programming Paradigm - -
6 - 15
2.1 Different paradigms - -
3 Mixed-level languages - -
1. INTRODUCTION
In the last unit 1, learners have explored the fundamentals of programming language design, including
the performance and features of different languages and their development over time. They studied
the structures of programming languages and the differences between compilation and
interpretation, including the stages of compilation and types of compilers and interpreters and also,
they learned about the comparison between compilers and interpreters. The unit also covered
programming environments, with a focus on Integrated Development Environments (IDEs) .
In this unit, learners will learn that the study of programming languages is an essential component of
computer science education, providing deep understanding of how different languages address
problems and how they can be efficiently employed in software development. This field includes
understanding various paradigms, studying mixed-level languages, and knowing the importance of
learning programming languages.
The field of programming languages covers a broad range of subjects that are essential to
understanding the variety and complexity of software development. It is essential to understand the
various programming paradigms among these. Programming paradigms are the foundational
methodologies or approaches to programming that determine how problems are resolved and how
code is organized. The programming paradigms encompass procedural, object-oriented, functional,
and logic programming, each providing distinct viewpoints and resources for addressing problems.
Procedural programming prioritizes a series of instructions, object-oriented programming structures
code around objects and their interactions, functional programming highlights the assessment of
mathematical functions without any side effects, and logic programming is founded on formal logic.
There are multiple reasons for studying programming languages. Learning several programming
languages can improve your proficiency in conveying complex ideas through code, as each language
provides distinct structures and abstractions. Having a thorough understanding of programming
languages facilitates the selection of the most suitable language for a specific task, hence enhancing
efficiency and effectiveness. Also, it enhances the learning of new languages by establishing a strong
base of essential programming concepts. Gaining an in-depth knowledge of the implementation
complexities of programming languages results in making more optimal design decisions, enhancing
program efficiency, and simplifying the process of identifying and fixing errors. Moreover, possessing
extensive expertise in programming languages enables you to optimize your utilization of the
languages you are currently familiar with by uncovering and implementing previously undiscovered
capabilities. Studying programming languages ultimately enhances the progress of computing by
facilitating improved assessment and acceptance of new languages.
To study this unit, learners should begin by participating in practical coding exercises in different
programming languages to gain an in-depth knowledge of diverse programming paradigms. Analyse
and differentiate the characteristics and applications of mixed-level languages by evaluating the
importance of acquiring expertise in numerous languages and consider how this information might
be utilized in your programming activities. Improve your understanding by actively participating in
both theoretical topics and practical challenges.
1.1. Objectives
After studying this unit, you should be able to:
2. PROGRAMMING PARADIGM
Programming paradigms include several approaches or frameworks for organizing programs or
programming languages. Each paradigm incorporates unique frameworks, attributes, and concepts
for addressing common programming issues. The existence of different programming paradigms is
similar to the existence of multiple programming languages. Various paradigms are more suitable for
different problem kinds, hence it is logical to use different paradigms for distinct program types.
Also, the methodologies that describe each paradigm have changed over time. Innovations in both
software and technology have brought up new approaches that were previously unavailable.
Programming paradigms are conceptual structures rather than physical languages or tools. They
represent a collection of concepts and procedures that have been widely accepted, followed, and
developed by the programming community.
Programming languages sometimes do not exactly adhere to a single paradigm. Certain programming
languages are intentionally created to align with a specific paradigm, containing characteristics that
facilitate that particular style of programming.
But there exist "multi-paradigm" languages that enable developers to write code following to several
paradigms (JavaScript and Python are prominent examples).
Also, programming paradigms are not mutually exclusive; it is feasible to include methodologies from
many paradigms in the same project without experiencing any problems.
Object-
Declarative
Oriented
Programming
Programming
i. Imperative Programming
Imperative programming involves providing the computer with specific and sequential
instructions to execute. The term "imperative" is used because as programmers, we clearly specify
the actions that the computer must perform in a highly particular manner.
Here, the program is instructed to initialize the variable "sum" to 0 and then iteratively add the
numbers from 1 to 10 to the variable "sum".
Observe that the instructions are carried out in a specific sequence, and each step alters the
program's state.
The programme is instructed to define a function named "factorial" that takes an integer "n" as its
parameter. The function applies recursion to get the factorial of n. The algorithm includes a base
case where it returns 1 if n is equal to 0 or 1. On the other hand, it yields the result of multiplying
n by the factorial of n - 1. Within the main function, we use a prompt that requests the user's input
of a numerical value. The value is then read and subsequently assigned to the variable num.
Subsequently, we call upon the factorial function, using the variable num as the input parameter,
and subsequently assign the resulting output to the variable result. Finally, we display the result
on the console, displaying the factorial of the provided integer.
The above C program computes the factorial of a number by using a recursive function,
demonstrating the concept of functional programming in an imperative language. The factorial
function is described as a recursive function that decrements the input number by one until it hits
the base case of zero, at which point it returns one. This solution uses the notion of recursion,
which is a fundamental element of functional programming, to solve the problem. The number 5
is used as an example input in the main function. The factorial function is invoked with this
number, and the resulting value, 120, is displayed. The recursion operates by multiplying the
current number n with the factorial of n-1, progressively accumulating the result through a series
of recursive operations until the base case is achieved. This showcases the application of
functional approaches in languages such as C. The program successfully computes the factorial of
5 as 120, and this is the output.
to its focus on logic rather than implementation. By separating the what from the how, it allows
developers to focus on problem-solving while frameworks or interpreters handle the operational
details, making it a preferred approach for modern software development. Its advantages, such as
improved readability, scalability, and reduced cognitive load, make it highly applicable across
functional programming, logic programming, and markup languages.
int main() {
int nums[] = {1, 4, 3, 6, 7, 8, 9, 2};
return 0;
}
Output:
In this example, we want to use a more declarative method in C to add up only the even numbers
in an array. We create a method called isEven that checks if a number is even. The primary logic
is contained in the sumIf function, which hides the process of iterating and checking conditions.
You can pass an array, its size, and a predicate function to this function. As it goes through the list,
it applies the predicate to each item and adds up the ones that meet the condition. We set up an
array of integers in the main method and then use this array and the isEven predicate to call sumIf.
Finally, we print the sum of all the even numbers. This method hides the details of how the sum is
figured and instead focuses on what needs to be done, which is in line with the ideas of declarative
programming. The program's result, "Sum of even numbers: 20," shows that only the even
numbers in the array were added up, which is what was meant by "declarative style."
v. Object-Oriented Programming
The fundamental principle of Object-Oriented Programming (OOP) is to partition responsibilities
into discrete entities that are implemented as objects. An entity will consist of a certain collection
of properties and methods that are related to it.
private:
std::string name; // Property to store the name of the dog
int age; // Property to store the age of the dog
};
int main() {
// Creating an instance of Dog
Dog myDog("Buddy", 3);
// Calling the bark method
myDog.bark();
return 0;
}
Output:
The above C++ program defines a Dog class that contains properties and methods, demonstrating
the fundamentals of Object-Oriented Programming (OOP). The Dog class contains two private
instance variables, name, and age, which are used to hold the name and age of the dog, respectively.
When a Dog object is created, a public constructor is available to initialize these properties. Also,
there exists a publicly accessible method called "bark" that generates an output message
indicating that the dog is currently emitting a barking sound. In the main function, an instance of
the Dog class named myDog is created with the name "Buddy" and age 3. Next, we invoke the bark
method on the myDog object, resulting in the output "Buddy is barking" being displayed on the
console. This program demonstrates the fundamental OOP principles of encapsulation, which
involves organizing relevant attributes and methods within the Dog class. It also showcases the
utilization of constructors and methods to instantiate and manipulate objects.
3. MIXED-LEVEL LANGUAGES
Mixed-level languages, or hybrid languages, are computer languages that combine characteristics
from both low-level and high-level programming paradigms. These languages enable the combination
of low-level language features, such as direct memory access and manual memory management, with
high-level language features, such as object-oriented and functional programming constructs,
providing both flexibility and control as well as abstraction and ease of use. Employing this hybrid
methodology can result in software that is both more efficient and resilient, as developers can fine-
tune critical sections of the code for optimal performance while utilizing higher-level structures to
handle intricate aspects.
ii. Rust: Used in the fields of systems programming, web assembly, and network services.
iii. Swift: Primarily used for iOS and macOS development, as well as various other applications.
iv. Objective-C: Enables programming at both low and high levels within a single language.
Combines low-
Design Ensures safety Safe, fast, Extends C with
level features
Philosophy and concurrency expressive OOP
with OOP
Fearless
Thread-based Thread-based Thread-based
Concurrency concurrency
concurrency concurrency concurrency
model
Verbose, C-like
Complex and Modern and Clean and
Syntax with OOP
verbose concise readable
extensions
Various compilers
Compiler rustc Swift compiler Clang
(GCC, Clang)
Interoperable
Interoperable Interoperable Interoperable
Interoperability with Objective-C
with C with C with C
and C
• Manual resource management, including manual memory management and low-level control,
can result in mistakes such as memory leaks, buffer overflows, and dangling pointers if not
properly managed.
• Comparison between Safety and Performance Considerations: Achieving both memory and
thread safety while retaining optimal performance can be a difficult task that might require
making compromises.
• Debugging and maintenance: Debugging low-level code may present greater challenges and
consume more time in comparison to high-level code, particularly when addressing memory-
related problems.
• Tooling and Ecosystem: While many mixed-level languages include robust tooling support,
others may have less developed ecosystems, which can impact productivity.
• Concurrency Issues: Effectively managing concurrency can be complex, necessitating
developers to possess a deep understanding of concurrency models and synchronization
techniques.
int main() {
std::vector<int> arr = {1, 2, 3, 4, 5};
int result = sum(arr);
std::cout << "Sum of the array: " << result << std::endl;
return 0;
}
Output:
The given C++ program demonstrates calculating the sum of an array of integers using a combination
of low-level and high-level operations, showcasing the mixed-level language capabilities of C++. The
program begins with including the necessary header <iostream> for input-output operations and
<vector> for using the std::vector class (The std::vector is a part of the C++ Standard Library and is
defined in the <vector> header. It is a dynamic array that can change size, allowing elements to be
added or removed at runtime. The std::vector class provides several functionalities that make it easier
to manage arrays, including automatic memory management, bounds checking, and various member
functions to manipulate the elements). A function sum is defined, which takes a constant reference
to a std::vector<int> and returns the total sum of its elements. Inside the function, a local variable
total is initialized to 0, and a range-based for loop iterates over each element of the vector, adding
each element's value to total. The final sum is then returned. In the main function, a std::vector<int>
named arr is initialized with the values 1, 2, 3, 4, and 5. The sum function is called with arr as an
argument, and the result is stored in the variable result. Finally, the result is printed to the console
using std::cout. This program effectively uses C++'s high-level vector operations for dynamic array
handling while employing low-level iteration and sum calculation to achieve the desired functionality.
Rust Program:
// Mixed-Level: Using Rust's high-level Vec with manual iteration for sum calculation
fn sum(arr: &Vec<i32>) -> i32 {
let mut total = 0;
for &num in arr.iter() {
total += num;
}
total
fn main() {
let arr = vec![1, 2, 3, 4, 5];
let result = sum(&arr);
println!("Sum of the array: {}", result);
}
Output:
The above program uses Rust's high-level Vec type for dynamic arrays and manual iteration to
calculate the sum.
Function Definition:
• fn sum(arr: &Vec<i32>) -> i32 {
• Defines a function named sum that takes a reference to a Vec<i32> (vector of 32-bit integers)
as an argument and returns an i32 (32-bit integer).
• fn is the keyword for defining functions.
• arr: &Vec<i32> specifies that arr is a reference to a vector of 32-bit integers, meaning the
vector itself is not copied or moved, ensuring efficient access.
Variable Declaration:
• let mut total = 0;
• Declares a mutable variable total and initializes it to 0.
• let is the keyword for variable declaration.
• mut indicates that the variable is mutable and can be changed.
For Loop:
• for &num in arr.iter() {
• Iterates over each element in the vector arr.
• for is the keyword for loop iteration.
• &num destructures each element, obtaining a reference to the value.
• arr.iter() returns an iterator over the elements of arr.
Update Total:
• total += num;
• Adds the value of num to total for each iteration.
• += is the operator for addition and assignment, updating total with the new sum.
Return Statement:
• total
• Returns the final value of total after the loop completes.
• In Rust, the last expression in a function is implicitly returned if it is not followed by a
semicolon.
Main Function:
• fn main() {
• Defines the main function, which is the entry point of the Rust program.
Vector Initialization:
• let arr = vec![1, 2, 3, 4, 5];
• Creates a vector arr with the elements 1, 2, 3, 4, and 5.
• vec! is a macro that creates a new Vec<i32> initialized with the specified elements.
Function Call:
• let result = sum(&arr);
• Calls the sum function, passing a reference to the vector arr.
• The result of the function call is stored in the variable result.
Print Result:
• println!("Sum of the array: {}", result);
• Prints the result to the console.
• println! is a macro for printing to the standard output.
• "Sum of the array: {}" is a format string where {} is replaced by the value of result.
The design and implementation of computer languages are frequently considered subjective, missing
defined organizing principles and universally recognized truths. Research laboratories and
enterprises employ numerous languages daily, each with its supporters and critics. Some argue that
all programming languages are essentially equal, with the only difference being a question of
individual preference. Can we truly assert that the computer programming language Java is superior
(or inferior) to C++? Is Scheme superior to Lisp? Is machine-level language superior to either of them?
The superiority of programming languages and the reasons behind it remain a subject of inquiry. The
study of the principles of programming languages facilitates an improved understanding of the
fundamental logic of programming languages, as well as their supporters and critics. It also facilitates
understanding of programming language paradigms.
• Willingness to Learn New Languages: Knowing that another language is better suited to a
particular problem can motivate you to learn it, thereby expanding your skill set and making
you more adaptable.
5. SUMMARY
In this unit, learners have studied that Studying programming languages is an essential part of
computer science education, as it is necessary for understanding the many approaches and tools used
in software development. This thorough review includes various programming paradigms, mixed-
level languages, and the reason for studying programming languages. These elements collectively
expand a programmer's perspective, enhance problem-solving abilities, and optimise programming
practices in terms of efficiency and effectiveness.
Mixed languages integrate characteristics from both low-level and high-level programming
paradigms. These languages provide the versatility and command that is commonly seen in low-level
languages, such as direct access to memory and manual management of memory, while also offering
the abstraction and user-friendliness that is characteristic of high-level languages. By employing this
hybrid method, developers can enhance the efficiency of code that is essential to performance, while
still ensuring that it remains easy to understand and maintain. Mixed-level languages have the
capability to alternate between low-level and high-level elements, allowing for precise manipulation
of hardware and memory to optimise speed, while utilising high-level elements to enhance code
organisation and readability. In addition, they integrate safety measures to mitigate prevalent
programming errors. Some examples of mixed-level languages are C++, Rust, and Swift. C++
integrates low-level functionalities such as pointers and manual memory management with high-
level object-oriented programming constructs, making it extensively used for game development,
systems programming, and applications that demand superior performance. Rust offers precise
management of memory and concurrency, guaranteeing safety through its ownership structure. This
makes it well-suited for systems programming and applications that demand exceptional
dependability and performance. Swift is a programming language that prioritises safety and
performance. It has a user-friendly syntax and is optimised for developing programmes on iOS and
macOS. It is widely used for creating applications for Apple platforms. Multilevel languages provide
flexibility that is appropriate for a diverse array of applications, allowing programmers to create
highly efficient code for areas that require optimal speed, while also ensuring safety and minimising
runtime errors. Nevertheless, they also provide difficulties, necessitating understanding of both
fundamental and advanced programming principles, which might be complex. Manual resource
management in these programming languages is susceptible to errors if not carefully managed, such
as memory leaks and buffer overflows.
Learning programming languages offers numerous of advantages that improve a programmer's skills
and contribute to the progress of computing. Having a comprehensive understanding of programming
languages and constructs improves problem-solving abilities by increasing the range of terms and
concepts that a programmer can utilize. Principles acquired in one language can be utilised in other
languages, even if those languages do not inherently accommodate those structures. Proficiency in
various languages empowers programmers to select the most appropriate language for a given
assignment, hence enhancing their decision-making abilities and adaptability. Knowing many
languages can serve as a source of inspiration to acquire proficiency in a new language, especially if
it is more suitable for addressing a specific situation. Gaining a strong foundation of fundamental
programming ideas simplifies the process of acquiring new programming languages, as having a prior
understanding of many languages may help in the learning of more languages. Acquiring knowledge
about the implementation of languages enhances understanding of their design and the reasoning
behind their structures, facilitating the creation of efficient code and the successful resolution of
debugging problems. Acquiring knowledge about the fundamental concepts of programming
languages is essential for developing proficiency and effectiveness as a programmer.
6. SELF-ASSESSMENT QUESTIONS
Multiple choice Questions
1 Which paradigm focuses on sequences of commands to change a program's state?
A) Functional Programming
B) Object-Oriented Programming
C) Imperative Programming
D) Declarative Programming
2 Which programming paradigm treats computation as the evaluation of mathematical
functions and avoids changing state?
A) Procedural Programming
B) Imperative Programming
C) Object-Oriented Programming
D) Functional Programming
3 Which of the following is an example of a mixed-level language?
A) Java
B) Rust
C) Python
D) SQL
4 In object-oriented programming, which concept allows objects to inherit properties and
methods from other objects?
A) Encapsulation
B) Polymorphism
C) Inheritance
D) Abstraction
5 Which paradigm is best suited for database queries?
A) Imperative Programming
B) Functional Programming
C) Procedural Programming
D) Declarative Programming
6 Which language is primarily used for iOS and macOS development?
A) Java
B) Swift
C) C++
D) Rust
7 Which feature of mixed-level languages helps prevent common programming errors like
buffer overflows?
A) Performance optimization
B) Flexibility
C) Safety features
D) Abstraction
8 Which of the following languages is an example of an imperative language?
A) Haskell
B) Prolog
C) C
D) Lisp
9 What type of errors do mixed-level languages aim to prevent through safety features?
A) Syntax errors
B) Runtime errors
C) Memory safety errors
D) Logic errors
Fill in the Blanks:
10 The programming paradigm that focuses on what to achieve rather than how to achieve it is
called _____________________.
11 __________ ensures memory safety without using a garbage collector through its ownership
system.
12 ____________ avoids changing state and mutable data.
True or False:
15 Column A Column B
Declarative Programming C
7. GLOSSARY
Financial Management is concerned with the procurement of the least cost funds, and its effective
Object-Oriented
It is a programming paradigm that revolves around the idea of "objects"
Programming -
that consist of both data and methods.
(OOP)
A function in which the output is solely defined by the input values and
Pure Function -
does not produce any significant side effects.
8. TERMINAL QUESTIONS
1. Define Programming Paradigms and mention the different paradigms.
2. Elucidate Imperative programming with an example.
3. Explain procedural Programming with an example.
4. Write a program to compute the factorial of a number using functional programming and explain
the same.
5. Write a program to perform the addition of even numbers using declarative programming.
6. Explicate the essential principles of Object-oriented Programming.
7. Explain the characteristics of Mixed-level languages.
8. Compare and contrast Mixed-level languages.
9. Write a program to perform a sum operation on array elements using Rust language.
10. Write a short note on the importance of studying programming languages.
9. ANSWERS
Self-Assessment Questions
1. Imperative Programming
2. Functional Programming
3. Rust
4. Inheritance
5. Declarative Programming
6. Swift
7. Safety features
8. C
9. Memory safety errors
10. Declarative programming
11. Rust
12. Functional Programming
13. True
14. False
15. Imperative Programming - C
Object-Oriented Programming - Java
Functional Programming - Haskell
Declarative Programming - SQL
Mixed-Level Language – Rust
10. REFERENCES
1. Robert W. Sebesta, “Concepts of Programming Languages”, 2012, Tenth Edition, . ISBN 10: 0-13-
139531-9.
2. Programming Paradigms – Paradigm Examples for Beginners (freecodecamp.org)
3. Programming Paradigms PDF | PDF | Programming Paradigms | Functional Programming
(scribd.com)
DCA1209
PRINCIPLE OF PROGRAMMING
LANGUAGES
Unit: 3 - Names, Scope, and Bindings 1
DCA1209: Principle of Programming Languages
Unit – 3
Names, Scope, and Bindings
TABLE OF CONTENTS
Fig No /
SL SAQ / Page
Topic Table /
No Activity No
Graph
1 Introduction - -
4-5
1.1 Objectives - -
2 Names - - 6-9
3 Scopes - -
1. INTRODUCTION
In the previous unit, learners studied various programming paradigms, exploring the fundamental
concepts and examples of imperative, procedural, functional, declarative, and object-oriented
programming. They delved into mixed-level languages, understanding how they combine low-level
and high-level programming features. Learners compared different mixed-level languages, examined
their advantages, such as flexibility, performance optimisation, and safety, and reviewed sample
programs to see these concepts in action.
In this unit, learners will study the high-level programming languages and derive their name from the
high degree of abstraction they provide compared to the assembly languages they replaced. This
abstraction separates language features from the details of specific computer architectures,
promoting machine independence and ease of programming. The early languages like Fortran, Algol,
and Lisp were designed with these goals in mind, allowing programs to run on various machines and
simplifying them for human to understand.
Machine independence means that a programming language should not depend on the features of any
particular machine's instruction set for efficient implementation. Although machine dependencies
can still pose problems, the push for machine independence has largely shaped language design over
the past decades. In contrast, ease of programming is a more elusive goal, influencing every aspect of
language design and evolving through a mix of aesthetics and trial and error.
Names, scope, and bindings are fundamental concepts in language design. A name is a symbolic
identifier representing variables, constants, operations, types, and other elements. Names simplify
programming by allowing references to higher-level concepts rather than low-level details like
memory addresses. This abstraction reduces complexity, enabling programmers to focus on the
purpose and function of code rather than its implementation details. For instance, subroutines and
classes provide control and data abstractions, respectively, by hiding complex code and data
representation behind simple interfaces.
Key names-related issues include binding time, storage allocation, scope rules, and referencing
environments. Binding time refers to when a name is bound to the entity it represents, impacting
various design decisions in language implementation. Storage allocation mechanisms manage the
allocation and deallocation of memory for objects, with distinct lifetimes for objects and their name
bindings. Scope rules define the region within a program where a name-to-object binding is valid, and
the complete set of bindings at any point is known as the current referencing environment.
Additional complexities in naming include aliasing, where multiple names refer to the same object;
overloading, where a single name refers to different objects depending on the context and
polymorphism, where an object can have multiple types based on the context or execution history.
Understanding these concepts is important for mastering high-level programming languages and
designing efficient, maintainable, and portable code.
This unit is important for learners to understand how programming languages manage variable
names, scope rules, and memory allocation, which are fundamental for writing efficient and error-
free code. Learners have to focus on each topic, like scopes and binding times, to grasp how variables
interact within different program contexts and review object lifetime and storage management
techniques to optimise memory use. This knowledge will enhance your coding skills and deepen
learner's knowledge of language implementation intricacies.
1.1. Objectives
After studying this unit, you should be able to:
2. NAMES
A name is a sequence or string of characters utilised to identify an entity within a program, such as a
variable, function, type, or module. Names are essential for readability and maintainability, as they
allow programmers to refer to entities using descriptive identifiers rather than low-level memory
addresses.
Length of Names
There are specific guidelines regarding the allowable length of names in every programming language:
Form of Names
In most programming languages, names generally follow a common structure:
Naming Conventions
Underscores were frequently employed in names during the 1970s and 1980s, but their popularity
has decreased. Camel case notation has mostly replaced this approach in languages based on C where
the first word is lowercase, and subsequent words are capitalised (e.g., myStack).
• PHP: Names of the Variables must start with a dollar sign ($).
• Perl: the initial unique symbol or special character of a variable name ($, @, or %) indicates
its type in a context-specific sense.
• Ruby: the initial unique symbol or special character of a variable name (@ or @@) indicates
instance variables or class variables, respectively.
In numerous computer languages, particularly those originating from the C family history, the
distinction between uppercase and lowercase letters in names are treated as different, making these
languages case-sensitive. For example, in C++, pen, PEN, and Pen are three different identifiers. This
can negatively impact readability because names that look similar can refer to different entities,
violating the principle that similar-looking constructs should have similar meanings. While some
languages like C mitigate this by conventionally using lowercase for variable names, languages like
Java and C# face more challenges due to predefined names using mixed cases. For instance, Java's
parseInt method must be written exactly as such, and variants like ParseInt or parseint will not be
recognized. This case sensitivity issue primarily affects writability, making it harder to write correct
programs as programmers must remember the exact case usage, a rigidity enforced by the compiler.
Special Words:
Programming languages have special words that make them easier to read by pointing out actions
and telling the difference between the syntax of sentences and programmes. Most of the time, these
unique words are called reserved words or keywords.
• Reserved Words: Programmers cannot redefine these. The language uses them exclusively to
avoid confusion. Reserved words are considered a better design choice because their
meanings are fixed, preventing potential misunderstandings. For example, most languages
cannot use reserved words as variable names.
• Keywords: Keywords are predefined, reserved words in programming languages that have
special meanings and are used to perform specific operations or declare specific constructs.
These words cannot be redefined or used for purposes other than their intended functionality,
as doing so would conflict with the syntax and semantics of the language.
A major problem with reserved words is that an excessive number of them can make it challenging
for programmers to create variable names that aren't reserved.
For example, COBOL has around 300 reserved words, including commonly used names like LENGTH
and COUNT, making it challenging to avoid conflicts.
Variables:
A program variable is a conceptual representation of a computer memory cell or a group of cells.
While programmers commonly see variable names as references to memory locations, variables have
a broader scope than simple names. The transition from machine languages to assembly languages
substituted numeric memory locations with names, improving programmes' readability and
maintainability.
Six attributes can define a variable: name, address, value, type, lifetime, and scope.
Names
Scope Address
Variables
Lifetime Value
Types
Name: The variable's name serves as the identifier that programmers use to reference the variable
within the code. This name acts as a symbolic representation, enabling programmers to refer to
memory locations in an easily understandable way.
For example, in the statement int age = 25; age is the variable's name.
Address: A variable's address refers to its location in the computer's memory, which can change
while the program is running.
For example, local variables within subprograms can receive different addresses with each call
because they are assigned from the runtime stack. This concept is known as instantiation, i.e., the
same variable can have different addresses at different times. The variable's address, or its l-value, is
important when the variable's name is on the left of an assignment statement.
Multiple variables can assign a similar address, creating aliases. Aliasing occurs when different
variable names refer to a similar memory location. This can hinder readability and complicate
program verification because changes to one alias affect all others.
For example, if sum and total are aliases, modifying sum also affects total. Remembering all aliases
and their implications can be challenging for the programmer, making code harder to read and verify.
Type: A type of a variable specifies the range of values it can hold and the actions that can be executed
on those values. For instance, in Java, the int type can hold values between -2147483648 and
2147483647, allowing arithmetic operations like addition, subtraction, multiplication, division, and
modulus.
Value: The variable’s value corresponds to the data stored in the memory cell or cells linked to that
variable. Understanding computer memory becomes more accessible when it is conceptualised as
abstract cells rather than real ones. Physical memory cells are commonly byte-sized, consisting of
eight bits, which are frequently insufficient in size for most programme variables.
An abstract memory cell is the minimum amount of memory required to store a variable. For instance,
a floating-point value may physically occupy four bytes, but it is considered to occupy only one
abstract memory cell. Each basic, unorganised category is considered to occupy a singular abstract
entity.
This value is often referred to as the variable's r-value, required when the variable's name is used on
the right side of an assignment statement. To retrieve the r-value, the l-value, or the variable's address,
must be identified first, which can involve a complicated procedure.
Lifetime: A variable's lifetime refers to the span of time during which it occupies memory. This
duration starts when the variable is allocated and ends when it is deallocated. For instance, a local
variable within a function exists only while that function is running. Once the function finishes, the
variable is destroyed, and its memory is released.
Scope: The area of code a variable can access is called its scope.
3. SCOPES
The scope of a variable is the set of statements in which the variable is accessible and can be
referenced, thereby allowing it to be used within those statements.
The scope rules of a programming language determine how each occurrence of a name (variable) is
associated with its declaration. In functional languages, these rules also determine how names are
linked to expressions.
An important function of scope rules is establishing a relationship between the declarations and
attributes of variables declared outside the presently running subprogram or block.
Declaring a variable inside a program unit or block context makes it local. Conversely, variables that
are declared outside of a programme unit or block yet visible within it are called nonlocal variables.
Among nonlocal variables, global variables stand out due to their program-wide accessibility.
In static scoping, the scope of a variable can be determined before program execution. This allows
both human readers and compilers to identify the type of each variable by examining the source code,
enhancing readability and predictability.
Static-scoped languages are categorised into two groups: those that permit nested subprograms,
creating nested static scopes, and those that do not. A few languages (such as Ada, JavaScript, Common
LISP, Scheme, Fortran 2003+, F#, and Python) permit nested subprograms, while C-based languages
do not.
hierarchical search process ensures that the correct variable is accessed in nested subprograms,
maintaining the integrity of variable scopes.
Example Program 1: Consider a JavaScript function, big, which contains two nested functions, sub1
and sub2:
function big() {
function sub1() {
var x = 7;
sub2();
}
function sub2() {
var y = x;
}
var x = 3;
sub1();
}
In the above code i.e., example program 1, the variable x referenced in sub2 is linked to the x defined
in big. This is because the search for x starts in sub2, and when it isn't found there, the search
continues in the static parent, big. The x in sub1 is ignored since sub1 is not a static ancestor of sub2.
In static scoping languages, some variable declarations can be obscured from certain code segments.
For example, in big, x is declared in both big and sub1. Within sub1, any reference to x points to the
local x, thus hiding the x in big.
In Ada, variables hidden in ancestor scopes can be accessed using qualified references that specify the
ancestor scope's name. If big were written in Ada, the x in big could be accessed in sub1 by using big.x.
Blocks:
Many programming languages enable the creation of new static scopes within executable code. This
idea was introduced in ALGOL 60, allowing specific code segments to possess their own local
variables with limited scope, which are usually allocated dynamically upon entry and deallocated
upon exit. Such sections, called blocks, are fundamental to block-structured languages.
In C-based languages, any compound statement can create a new scope, called a block, by using
matched braces for declarations.
For example:
if (list[i] < list[j]) {
int temp;
temp = list[i];
list[i] = list[j];
list[j] = temp;
}
Here, the scope of temp is limited to the block defined by the braces. Blocks can be nested within
larger blocks, with variable references resolved by searching enclosing scopes.
In this example, the count variable in the while loop hides the count variable in the function sub.
Declaration Order:
In older versions of C, such as C89 and some other programming languages, all variable declarations
within a function must be placed at the beginning of the function. Other languages like C99, C++, Java,
JavaScript, and C# permit variable declarations to occur at any point within a program unit, provided
that it is permissible to execute a statement.
Variable declarations in these languages can create scopes that are not essentially associated with
compound statements or subprograms.
For example, in C99, C++, and Java, a local variable's scope begins when it is declared and continues
until the conclusion of the block containing its declaration.
But, in C#, the scope of a variable declared within a block encompasses the entire block, regardless of
where the declaration occurs, provided it is not within a nested block. Even with this, C# still
mandates that variables must be declared before they are used in the code. Therefore, even though
the scope extends to the top of the block, a variable cannot be utilised before its declaration.
You can declare local variables anywhere in JavaScript code, but their scope will always cover the
entire function. If a variable is used before its declaration, it is assigned the value undefined.
Additionally, in the ‘for’ statements of C++, Java, and C#, variables can be defined within the
initialization expressions. Initially, in C++, the scope of such variables extended to the end of the
smallest enclosing block. But, in the standardized version of C++, as well as in Java and C#, the scope
is limited to the construct itself.
Example:
void fun() {
// ...
for (int count = 0; count < 10; count++) {
// ...
}
// ...
}
In the given code, the function fun includes a for loop with a variable declaration within its
initialization expression. This is a common practice in modern versions of C++, Java, and C#. The
variable count is declared and initialized within the for loop's initialization section (int count = 0). In
these languages, the scope of count is limited to the for loop itself. This means count is only accessible
and usable within the loop's body and not outside of it. This approach helps in keeping the scope of
variables minimal, reducing the likelihood of errors from variable misuse or unintended interactions.
The rest of the function can contain other code, but count cannot be referenced outside the for loop.
Global Scope:
Some programming languages like C, C++, PHP, JavaScript, and Python allow variables to be defined
outside functions, creating global variables that can be accessed by those functions. In C and C++,
global variables require both declarations and definitions, where declarations specify types and
attributes without allocating storage, and definitions allocate storage. Multiple declarations can exist,
but only one definition is allowed. A variable declared outside functions in one file is defined in
another. A global variable can be hidden by a local variable with the same name, but it can still be
accessed using the extern keyword or scope resolution operator (::) in C++.
In PHP, variables are implicitly declared when they appear in statements. Variables declared outside
any function are global and can be accessed in functions using the $GLOBALS array or a global
declaration.
The given PHP code demonstrates the use of global and local variables within a function and
illustrates how to access global variables when a local variable with the same name exists. Initially,
two global variables, $day and $month, are defined with the values "Monday" and "January,"
respectively. The calendar function is then defined, within which a local variable $day is assigned the
value "Tuesday." This local variable masks the global $day within the function's scope.
To access the global $month variable within the function, the global keyword is used, making $month
accessible and allowing its value to be printed. Inside the function, print "local day is $day <br />";
outputs "local day is Tuesday," reflecting the value of the local $day.
Next, the global $day is accessed through the $GLOBALS array, a PHP superglobal that contains
references to all global variables. The statement $gday = $GLOBALS['day']; retrieves the value of the
global $day ("Monday") and assigns it to $gday. The line print "global day is $gday <br />"; then prints
"global day is Monday." Finally, the function prints the global $month using print "global month is
$month <br />";, resulting in "global month is January." When the calendar function is called, the
output is:
In JavaScript, global variables work similarly to PHP but cannot be accessed if a local variable with
the same name exists in a function. Python's handling of global variables is unique; variables are
declared when assigned values. To modify a global variable within a function, it must be declared as
global using the global keyword.
For example:
day = "Monday"
def tester():
global day
print("The global day is:", day)
day = "Tuesday"
print("The new value of day is:", day)
tester()
The function tester is defined to demonstrate how to reference and modify this global variable within
a function. When the tester function is called, it first uses the global keyword to specify that it is
referring to the global variable day. This allows the function to access and modify the global variable
instead of creating a new local variable. The function then prints the current value of day, which is
"Monday". After that, it updates the value of day to "Tuesday" and prints this new value.
function big() {
function sub1() {
var x = 7;
}
function sub2() {
var y = x;
var z = 3;
}
var x = 3;
}
In the above code, the function big which contains two nested functions sub1 and sub2. Within sub1,
a local variable x is declared and initialized to 7. In sub2, another variable y is assigned the value of x,
and z is set to 3. With dynamic scoping, the reference to x in sub2 can refer to either x from sub1 or
the outer x depending on the calling sequence of these functions.
Dynamic scoping works by searching for variable declarations starting from the local scope and
moving up through the calling sequence of functions. If a variable is not found in the local declarations,
the search continues in the calling function, and then in that function's caller, and so on, until a
declaration is found or a runtime error occurs if none is found.
• Language Design Time: When the language is being designed, certain symbols and operations
are defined. For example, the asterisk (*) is bound to the multiplication operation.
• Language Implementation Time: During the implementation of the language, specific details
are decided. For instance, the data type int in C is bound to a range of possible values.
• Compile Time: When the source code is compiled, variables are connected to their data types.
For example, during compilation in Java, a variable is assigned a particular data type.
• Link Time: When different modules of a program are linked together, references to library
subprograms are resolved.
• Load Time: Variables can be linked to specific storage places when the programme is loaded into
memory.
• Run Time: During the execution of the program, some bindings occur dynamically, such as the
binding of variables declared within methods in Java.
Example:
Let’s consider the following Java Statement:
Sum = Sum + 10;
Various bindings and their binding times in this statement are:
Implicit Declarations:
Instead of using explicit declaration lines, implicit declarations use default rules to link variables
and types. In this case, a variable is implicitly declared when it appears for the first time in a
programme. The language processor will use this method. The language processor can be either a
compiler or an interpreter. Naming rules are a simple way to support implicit type linking. In
Fortran, for example, an identifier that isn't stated directly is declared based on its first letter.
Some identifiers, like those that start with I, J, K, L, M, or N (or their lowercase versions), are
automatically declared as Integer types. Other types are stated as Real types.
hard-to-diagnose errors. To avoid such issues, many Fortran programmers use the declaration
Implicit none, instructing the compiler not to implicitly declare any variables.
When a variable's type is statically bound, its name is permanently linked to that type. However,
in the context of dynamic type binding, the name is simply temporarily linked to the type. Names
are inherently associated with variables, which in turn are associated with types. Language
systems that employ dynamic type binding function in a significantly distinct manner compared
to those that utilise static type binding.
Dynamic type binding offers enhanced programming freedom as its primary feature. For example,
in a dynamically typed programming language, a programme intended to manipulate numerical
data can be constructed in a generic manner to handle any sort of numeric data. In dynamically
typed languages, the variables automatically adjust their type based on the provided data,
compared to statically typed languages where the data type must be predetermined.
Before the mid-1990s, most programming languages used static type binding, with exceptions like
some functional languages such as LISP. Since then, there has been a significant shift toward
languages that employ dynamic type binding, such as Python, Ruby, JavaScript, and PHP.
For example, in JavaScript, assigning a list of numbers to a variable changes its type to an array,
and reassigning a single number changes it to a scalar. This flexibility allows for dynamic
adjustments to variable types based on the data they hold i.e.,
Regardless of the previous type of the variable list, this assignment makes list the name of a single-
dimension array of length 2. If we then execute:
list = 47;
This demonstrates how dynamic type binding allows variables to adapt their types at runtime
based on the assigned value.
C# added dynamic type binding in its 2010 edition, enabling increased flexibility in managing
variables with unknown types until runtime. In the C# programming language, a variable can be
declared to utilise dynamic type binding by utilising the 'dynamic' keyword:
dynamic any;
This declaration is similar to declaring a variable of type object, as it allows any to be assigned a
value of any type. However, there are important differences. While an object type can hold any
data type, dynamic is particularly useful for interoperability with dynamically typed languages,
such as IronPython and IronRuby, which are .NET versions of Python and Ruby, respectively.
With dynamic, you may effortlessly handle data of unknown types originating from other sources.
This functionality is particularly useful in situations where the data type cannot be identified
during the compilation process. The dynamic keyword in C# allows for its usage in several
contexts such as class members, properties, method parameters, method return values, and local
variables.
In pure object-oriented languages such as Ruby, all variables are references and do not possess
fixed types. Instead, all data is represented as objects, and any variable can be used to refer to any
object. In other words, all variables are of the same type, which is a reference. This is distinct from
languages such as Java, where references are limited to a certain type of value. Ruby allows variables
to reference any object, offering considerable versatility.
Finally, languages with dynamic type binding are often implemented using pure interpreters rather
than compilers, as the types of variables are not known at compile time. Pure interpretation can be
significantly slower than executing compiled machine code. Although the time taken for dynamic type
binding is hidden within the overall interpretation time, it still adds to the performance overhead. In
contrast, statically typed languages are typically compiled into efficient machine code, resulting in
faster execution.
It is essential to differentiate between the names and the objects they refer to. Key events in this
context include:
The period between the creation and destruction of a name-to-object binding is known as the
binding's lifetime, while the time from the creation to the destruction of an object is its lifetime. These
lifetimes may not always align. An object can persist and remain accessible even if the name bound to
it is no longer valid.
For example, when a variable is passed by reference to a subroutine, the binding between the
parameter name and the variable has a shorter lifetime than the variable itself. On the other hand, a
binding may survive the object it references, leading to a dangling reference, which often indicates a
programming error. This can occur if an object allocated with C++'s new operator is passed by
reference and then deallocated (delete-ed) before the subroutine completes, leaving a binding to a
non-existent object.
Object lifetimes generally align with three main storage allocation mechanisms, which manage the
space for objects:
• Static Objects: These objects are assigned a fixed address that remains constant throughout
the program's execution.
• Stack Objects: Allocated and deallocated in a last-in, first-out (LIFO) manner, stack objects are
typically associated with subroutine (function) calls and returns i.e. when a function is called,
its local variables are pushed onto the stack, and when the function exits, they are popped off,
making stack management fast and efficient.
• Heap Objects: These objects can be allocated and deallocated anytime, providing flexibility
but requiring more complex and costly storage management algorithms. The heap is used for
dynamic memory allocation, allowing objects to persist beyond the scope of a single function,
but necessitating careful management to avoid fragmentation and memory leaks.
For example, in languages like Fortran, where recursion was initially unsupported, static allocation
can be used for local variables within subroutines. Since only one subroutine invocation can be active
at a time, the same memory locations can be reused for local variables across multiple calls,
eliminating the need for runtime allocation and deallocation. This method reduces overhead and
improves performance.
Many programming languages require constants to have values that can be determined at compile
time. These constants can be statically allocated, even if local to a recursive subroutine, because
multiple instances can share the same memory location. On the other hand, languages like C and Ada
allow constants whose values depend on runtime variables. Such constants must be allocated on the
stack if they are local to recursive subroutines.
Other than local variables and constants, compilers also manage other information associated with
subroutines, as shown in figure 1. This includes:
• Arguments and Return Values: Stored in registers, when possible, but sometimes require
memory space.
• Temporaries: Intermediate values from complex calculations, ideally kept in registers.
• Bookkeeping Information: Includes return addresses, references to caller stack frames,
saved registers, and debugging data.
• Arguments and Return Values: Located at the top of the frame for easy access by the called
subroutine.
• Local Variables: Stored within the frame for the subroutine's use.
• Temporaries: Intermediate values needed during calculations.
• Bookkeeping Information: Includes the return address, references to the caller's stack frame
(dynamic link), and saved registers.
The stack-based allocation method is maintained by the calling sequence, which includes code
executed by the caller before and after the call, and the prologue and epilogue of the subroutine itself.
This sequence ensures that each subroutine's frame is correctly managed on the stack.
The figure 2 illustrates the stack-based allocation of space for subroutines in a programming
environment. In this setup, each active subroutine call at runtime occupies a distinct frame on the
stack. The stack grows downward, towards lower memory addresses, as indicated by the arrow on
the left.
• Subroutine Frames: Each subroutine (A, B, C, and D) has its own frame on the stack. When a
subroutine is called, a new frame is pushed onto the stack. This frame contains the subroutine’s
local variables, temporaries, arguments, return values, and miscellaneous bookkeeping
information, including the return address.
• Frame Pointers: The frame pointer (fp) points to a known location within the frame of the
current subroutine, facilitating access to local variables and arguments. The stack pointer (sp)
points to the top of the stack, marking the next available location for a new frame.
• Call Sequence: The figure shows Subroutine A calling Subroutine B, which calls Subroutine C,
and so on. When Subroutine A calls B, the arguments and return address are placed at the top
of A's frame. Subroutine B's frame is then pushed onto the stack, containing its own local
variables and bookkeeping data. This process continues with each nested call adding a new
frame to the stack.
• Accessing Variables: Within each frame, local variables and temporaries are accessed using
offsets from the frame pointer. Arguments for called routines are positioned at the top of the
frame for easy access by the callee.
External Fragmentation:
The figure 3 explains the concept of external fragmentation in heap-based allocation. In the figure 3:
External fragmentation occurs when free memory is divided into small, non-contiguous blocks,
making it impossible to allocate a large block even if enough total free space exists. This scenario
happens because the free blocks are scattered throughout the heap, preventing the allocation of a
single contiguous block of the required size.
• Single Free List: A common approach is to maintain a single linked list of free blocks. When a
memory request is made, the list is searched to find a suitable block.
o First Fit: Allocates the first block large enough to satisfy the request.
o Best Fit: Searches the entire list to find the smallest block that fits the request. While it can
better preserve large blocks for large requests, it increases allocation time and often leads
to many small leftover blocks.
• Fragmentation:
o Internal Fragmentation: Occurs when a block larger than necessary is allocated, leaving
unused space within the block.
o External Fragmentation: Occurs when free space is scattered, preventing the allocation
of large contiguous blocks.
Some algorithms maintain separate free lists for different block sizes to moderate allocation costs.
These lists help manage memory more efficiently by pooling blocks of similar sizes together.
6. SUMMARY
In this unit, learners have studied the concept of name identifiers used to represent variables,
functions, types, and other entities within a program. These names form the basis for referencing and
manipulating data and functions. Understanding the rules and conventions for defining and using
names is important for writing clear and effective code.
Learners have studied the concept of scope, which defines the context within which a name is valid
and can be used. Scopes help manage the visibility and lifespan of variables and functions, ensuring
that names are appropriately accessible and preventing conflicts. Scope rules are fundamental to
resource management and program organization, enabling developers to create modular and
maintainable code.
Binding time refers to the point at which various attributes, such as values, storage locations, and
types, are associated with program entities. Understanding when and how these bindings occur is
important for understanding how programs execute and manage resources. Binding can happen at
different stages, including compile time and runtime, influencing program flexibility and performance.
Object lifetime and storage management are key to efficient memory use in programming. Static
allocation involves assigning fixed memory addresses to objects, providing efficiency but lacking
flexibility. In contrast, stack-based allocation uses a stack data structure to manage memory
dynamically, supporting subroutine calls and recursion. Heap-based allocation allows for dynamic
memory allocation and deallocation at any time, essential for handling complex data structures and
varying object sizes.
7. SELF-ASSESSMENT QUESTIONS
Multiple choice Questions
1 What is the primary purpose of variable names in programming languages?
A) To represent memory addresses
B) To provide human-readable identifiers
C) To execute loops
D) To define functions
2 Which of the following is an example of a variable scope?
A) Global
B) Local
C) Both a and b
D) None of the above
3 When is a variable's type typically bound in a statically typed language?
A) Runtime
B) Compile time
C) Load time
D) Execution time
4 Which storage allocation method allows variables to retain their values between function
calls?
A) Stack-based allocation
B) Heap-based allocation
C) Static allocation
D) Dynamic allocation
5 In which type of allocation are variables typically stored in a last-in, first-out order?
A) Static allocation
B) Heap-based allocation
C) Stack-based allocation
D) Dynamic allocation
Fill in the Blanks:
6 The _______ of a variable defines where the variable can be accessed in the program.
True or False:
8. GLOSSARY
Financial Management is concerned with the procurement of the least cost funds, and its effective
Stack-Based Memory allocation that uses a stack data structure to manage the storage
-
Allocation of local variables.
Heap-Based Memory allocation that allows for dynamic allocation and deallocation of
-
Allocation memory blocks.
A register that points to the start of the current stack frame, used to access
Frame Pointer -
function parameters and local variables.
9. TERMINAL QUESTIONS
1. Explain the difference between static and dynamic scoping with examples.
2. Describe the concept of binding time and its significance in programming languages.
3. Explain the concept of variable lifetime and its impact on program execution.
4. How does stack-based allocation work? Explain with an example.
5. What is heap-based allocation and what are its primary uses?
6. What is the role of the frame pointer in stack-based memory allocation?
7. Explain the importance of scope in programming languages with suitable examples.
8. Explain the concept of object lifetime and its relationship with storage management strategies in
programming languages.
9. What are the key differences between lexical and dynamic scoping, and how do they affect variable
binding and program execution?
10. Provide a detailed analysis of how different storage management techniques (static, stack-based,
and heap-based) are utilized in modern programming languages.
10. ANSWERS
Self-Assessment Questions
1. To provide human-readable identifiers
2. Both a and b
3. Compile time
4. Static allocation
5. Stack-based allocation
6. Scope
7. Binding
8. Lifetime
9. True
10. False
11. REFERENCES
1. Robert W. Sebesta, “Concepts of Programming Languages”, 2012, Tenth Edition, . ISBN 10: 0-13-
139531-9.
2. Michael L. Scott, “ Programming Language Pragmatics”, 2015, Fourth edition, Morgan Kaufmann
Publishers In, ISBN 13: 978-0-12-633951-2.
DCA1209
PRINCIPLE OF PROGRAMMING
LANGUAGES
Unit: 4 - Implementing Scope and Separate Compilation 1
DCA1209: Principle of Programming Languages
Unit – 4
Implementing Scope and Separate
Compilation
TABLE OF CONTENTS
Fig No /
SL SAQ / Page
Topic Table /
No Activity No
Graph
1 Introduction - -
4-5
1.1 Objectives - -
2 Scope Rules - -
2.2 Blocks - -
6 - 13
2.3 Declaration Order - -
1. INTRODUCTION
In the previous unit 3, learners studied the fundamentals of programming languages, including the
use of names as identifiers, the concept of scopes for managing visibility and lifetime of entities, and
binding times for associating attributes to variables. They explored different types of type bindings
and learned about object lifetime and storage management, covering static, stack-based, and heap-
based allocation. This foundation prepares them for more advanced topics in the next unit 4.
In this unit, learners will understand the scope rules in programming, which is essential for managing
the visibility and lifetime of variables and functions. Static scoping determines the scope of a variable
based on the program's structure, making it easier to predict variable behaviour. Blocks allow for
defining new scopes within a program, facilitating local variable declaration and management. The
declaration order determines the sequence in which variables and functions must be declared,
ensuring proper usage and avoiding reference errors. Global scope pertains to variables and functions
accessible throughout the entire program, providing a means for shared data. In contrast, dynamic
scope bases variable scope on the program’s execution flow, allowing more flexibility but potentially
causing unpredictable behaviour. The binding of referencing environments is another essential
concept; subroutine closures capture the environment in which a function is defined, preserving
variable bindings for later use. Learners will learn the first- and second-class subroutines, which
differentiate between subroutines that can be passed as arguments or returned from other
subroutines and those that cannot. Learners will also learn that separate compilation manages large
programs by dividing them into smaller, independently compiled modules, enhancing code
modularity and maintainability.
To learn this unit, learners should practice implementing small programs demonstrating different
scope rules and binding environments. Visualizing scope hierarchies and data flow through diagrams
can help them better understand static and dynamic scoping. Using debuggers to step through code
will provide real-time observations of variable scope and lifetime. Writing and compiling separate
modules will offer hands-on experience with separate compilations. Referring to language-specific
documentation is essential to grasp how these concepts are implemented in various programming
languages.
1.1. Objectives
After studying this unit, you should be able to:
2. SCOPE RULES
The scope of a variable refers to the specific range of statements within a programme where the
variable can be accessed and can be referenced, thereby allowing it to be used within those
statements.
The scope rules of a programming language determine how each occurrence of a name (variable) is
associated with its declaration. In functional languages, these rules also determine how names are
linked to expressions.
An important function of scope rules is establishing a relationship between the declarations and
attributes of variables declared outside the presently running subprogram or block.
Declaring a variable inside a program unit or block context makes it local. Conversely, variables that
are declared outside of a programme unit or block yet visible within it are called nonlocal variables.
Among nonlocal variables, global variables stand out due to their program-wide accessibility.
In static scoping, the scope of a variable can be evaluated or determined before program execution.
This allows both human readers and compilers to identify the type of each variable by examining the
source code, enhancing readability and predictability.
Static-scoped languages are categorised into two groups: those that permit nested subprograms,
creating nested static scopes, and those that do not. A few languages (such as Ada, JavaScript, Common
LISP, Scheme, Fortran 2003+, F#, and Python) permit nested subprograms, while C-based languages
do not.
hierarchical search process ensures that the correct variable is accessed in nested subprograms,
maintaining the integrity of variable scopes.
Consider a JavaScript function, big, which contains two nested functions, sub1 and sub2:
function big() {
function sub1() {
var x = 7;
sub2();
}
function sub2() {
var y = x;
}
var x = 3;
sub1();
}
In the above code, the variable x referenced in sub2 is linked to the x defined in big. This occurs
because the search for the value x starts in sub2, and when it isn't located or found there, the search
proceeds in the static parent, big. The x in sub1 is disregarded or ignored since sub1 is not a static
ancestor of sub2.
Some variable declarations from certain code segments can be obscured in static scoping languages.
For example, in big, x is declared in both big and sub1. Within sub1, any reference to x points to the
local x, thus hiding the x in big.
In Ada, variables hidden in ancestor scopes can be retrieved or accessed using qualified references
that specify the ancestor scope's name.
The value of x in big could be retrieved in sub1 using big.x if big were written in Ada.
2.2. Blocks
Many programming languages enable the creation of new static scopes within executable code. This
idea was introduced in ALGOL 60, allowing specific code segments to possess their own local
variables with limited scope, which are usually allocated dynamically upon entry and deallocated
upon exit. Such sections, called blocks, are fundamental to block-structured languages.
In C-based languages, any compound statement can create a new scope, called a block, by using
matched braces for declarations.
For example:
if (list[i] < list[j]) {
int temp;
temp = list[i];
list[i] = list[j];
list[j] = temp;
}
Here, the scope of temp is limited to the block defined by the braces. Blocks can be nested within
larger blocks, with variable references resolved by searching enclosing scopes.
void sub() {
int count;
// ...
while (...) {
int count;
count++;
// ...
}
// ...
}
In this example, the count variable in the while loop hides the count variable in the function sub.
These languages' variable declarations provide the ability to create or generate scopes that aren't
necessarily linked or associated to subprograms or compound expressions.
For example, in C99, C++, and Java, the scope of a local variable starts at the time of declaration and
continues until the conclusion of the block containing its declaration.
In C#, the scope of a variable declared within a block extends across the whole block, regardless of
where the declaration is located, as long as it is not within a nested block. However this, C# still
requires that variables be declared before they are used in the code. Therefore, even though the scope
extends to the top of the block, a variable cannot be utilised before its declaration.
You can declare local variables anywhere in JavaScript code, but their scope will always cover the
entire function. If a variable is used before its declaration, it is assigned the value undefined.
Additionally, in the ‘for’ statements of C++, Java, and C#, variables can be defined within the
initialization expressions. Initially, in C++, the scope of such variables extended to the end of the
smallest enclosing block. But, in the standardized version of C++, as well as in Java and C#, the scope
is limited to the construct itself.
Example:
void fun() {
// ...
for (int count = 0; count < 10; count++) {
// ...
}
// ...
}
In the given code, the function fun includes a for loop with a variable declaration within its
initialization expression. This is a common practice in modern versions of C++, Java, and C#. The
variable count is declared and initialized within the for loop's initialization section (int count = 0). In
these languages, the scope of count is limited to the for loop itself. This means count is only accessible
and usable within the loop's body and not outside of it. This approach helps in keeping the scope of
variables minimal, reducing the likelihood of errors from variable misuse or unintended interactions.
The rest of the function can contain other code, but the count cannot be referenced outside the for
loop.
global variables require declarations and definitions, where declarations specify types and attributes
without allocating storage, and definitions allocate storage. Multiple declarations can exist, but only
one definition is allowed. A variable declared outside functions in one file is defined in another. A
global variable can be hidden by a local variable with the same name, but it can still be accessed using
the extern keyword or scope resolution operator (::) in C++.
In PHP, variables are implicitly declared when they appear in statements. Variables declared outside
any function are global and can be accessed in functions using the $GLOBALS array or a global
declaration.
The given PHP code demonstrates the use of global and local variables within a function and
illustrates how to access global variables when a local variable with the same name exists. Initially,
two global variables, $day and $month, are defined with the values "Monday" and "January,"
respectively. The calendar function is then defined, within which a local variable $day is assigned the
value "Tuesday." This local variable masks the global $day within the function's scope.
To access the global $month variable within the function, the global keyword is used, making $month
accessible and allowing its value to be printed. Inside the function, print "local day is $day <br />";
outputs "local day is Tuesday," reflecting the value of the local $day.
Next, the global $day is accessed through the $GLOBALS array, a PHP superglobal that contains
references to all global variables. The statement $gday = $GLOBALS['day']; retrieves the value of the
global $day ("Monday") and assigns it to $gday. The line print "global day is $gday <br />"; then prints
"global day is Monday." Finally, the function prints the global $month using print "global month is
$month <br />";, resulting in "global month is January." When the calendar function is called, the
output is:
In JavaScript, global variables work similarly to PHP but cannot be accessed if there is a local variable
with an identical or the same name that exists in a function. Python's handling of global variables is
unique; variables are declared when assigned values. To change or modify a global variable within a
function, it must be declared as global using the global keyword.
For example:
day = "Monday"
def tester():
global day
print("The global day is:", day)
day = "Tuesday"
print("The new value of day is:", day)
tester()
The function tester is defined to demonstrate how to reference and modify this global variable within
a function. When the tester function is called, it first uses the global keyword to specify that it is
referring to the global variable day. This allows the function to access and modify the global variable
instead of creating a new local variable. The function then prints the current value of day, which is
"Monday". After that, it updates the value of day to "Tuesday" and prints this new value.
One significant advantage of static scope is predictability. Since the scope of variables is determined
at compile time, it becomes easier to predict and understand their behavior within the program. This
clarity simplifies debugging and maintenance, as developers can readily identify where variables are
declared and how they are used throughout the code. Additionally, static scope helps prevent
unintended side effects by limiting variable visibility. By confining the scope of variables to specific
blocks or functions, static scoping reduces the likelihood of variable name collisions and unintentional
modifications, enhancing code reliability and stability.
Even with its benefits, static scope has some disadvantages, primarily related to reduced flexibility.
The inability to change variable bindings at runtime can be limiting in scenarios where dynamic
behavior is desired. This restriction can make certain programming tasks more cumbersome and less
efficient, as developers may need to implement additional structures or workarounds to achieve the
desired functionality. In contrast to dynamic scope, static scope's fixed nature at compile time can
sometimes delay the ability to adapt and modify variable bindings based on runtime conditions,
potentially reducing the overall flexibility of the code.
function big() {
function sub1() {
var x = 7;
}
function sub2() {
var y = x;
var z = 3;
}
var x = 3;
}
In the above code, the function big which contains two nested functions sub1 and sub2. Within sub1,
a local variable x is declared and initialized to 7. In sub2, another variable y is assigned the value of x,
and z is set to 3. With dynamic scoping, the reference to x in sub2 can refer to either x from sub1 or
the outer x depending on the calling sequence of these functions.
Dynamic scoping works by searching for variable declarations starting from the local scope and
moving up through the calling sequence of functions. Suppose a variable is not found in the local
declarations. In that case, the search continues in the calling function, and then in that function's caller,
and so on, until a declaration is found or a runtime error occurs if none is found.
Dynamic scope offers significant flexibility by allowing variable bindings to change at runtime,
enabling more dynamic behaviour within programs. This adaptability is particularly useful in
scripting and interpreted languages where modifying variable bindings on the fly can simplify certain
variable access patterns. For example, in environments where functions or blocks of code need to
access variables defined in calling functions, dynamic scope can streamline the process, reducing the
need for complex parameter passing or global variables. This facilitation of use can make
programming more intuitive and efficient in scenarios where the call sequence heavily influences
variable usage.
One major disadvantage is unpredictability, as the binding of variables can change based on the call
sequence, making programs harder to understand and debug. Developers may struggle to track
variable origins and modifications, leading to increased potential for errors and unintended side
effects. Additionally, dynamic scope introduces performance overhead due to the need for runtime
lookups to resolve variable bindings. These lookups can impact the overall performance of the
program, especially in scenarios where variable resolution is frequent or complex. Thus, while
dynamic scope provides flexibility and ease of use, it can complicate program comprehension and
reduce execution efficiency.
Applying scope rules either when the subroutine reference is created or when it is called can
significantly affect program behavior.
print routine(r)
–– main program
...
threshold := 35
print selected records(people, older than, print person)
The above example highlights the significance of binding rules in a hypothetical programming
language, showcasing how static and dynamic scoping impact variable resolution and subroutine
execution. The code involves defining a person record type, a function to check if a person is older
than a specified threshold, and a procedure to print a person's details. A main procedure,
print_selected_records, also takes a database of person records, a predicate function, and a print
routine as parameters. This procedure sets a line length variable based on the output device type and
iterates over the database records, printing those that satisfy the predicate.
In the main program, the threshold is set to 35, and print_selected_records is called with the database,
the older_than predicate, and the print_person routine. Under static scoping, the older_than function
uses the threshold variable defined in the main program, as its binding is determined at compile time
based on the lexical structure of the code. Conversely, with dynamic scoping, the threshold variable
referenced by older_than would depend on the runtime call stack, potentially leading to different
bindings.
The line_length variable, set within print_selected_records and used by print_person, demonstrates
the importance of dynamic scoping for resolving nonlocal references at runtime. Additionally, the
example shows how passing subroutines as parameters can complicate binding, depending on
whether shallow or deep binding is used. Overall, this example emphasizes the crucial role of
understanding binding rules in programming language design and implementation, as they affect
variable visibility, program behavior, and subroutine interactions.
Scope rules determine which variables and subroutines are accessible at various points in a program.
Static scope rules define this access based on the lexical structure of the code—where the code is
written. Dynamic scope rules, however, determine access based on the order in which subroutines
are called during program execution.
Shallow binding delays the creation of the referencing environment for a subroutine until it is
actually called. This approach is typical in languages with dynamic scoping. The
print_selected_records procedure in an Algol-like language illustrates this concept in the given
example. This procedure traverses records in a database, using a predicate function to decide whether
to print each record and a subroutine (print_routine) to format the output. The print_person
subroutine, used to print records of people, relies on a non-local variable line_length to format its
output correctly. Shallow binding ensures that when print_person is called by print_selected_records,
it can access the line_length variable set by print_selected_records.
Deep binding, on the other hand, establishes the referencing environment for a subroutine when it
is first passed as a parameter, preserving that environment until the subroutine is called. This method
is essential when the referencing environment at the time of the subroutine's creation needs to be
maintained. In the above example, the older_than function is passed to print_selected_records. If
print_selected_records has a local variable named threshold, this could conflict with the global
threshold variable intended to be used by older_than. Deep binding ensures that older_than uses the
global threshold variable, not any local variable of the same name within print_selected_records. This
necessity for deep binding, particularly to solve issues known as the funarg (function argument
problem) problem in Lisp, underscores the importance of selecting appropriate binding strategies to
ensure program reliability and intended behavior.
In languages with dynamic scoping, shallow binding is typically the default, as it allows subroutines
to adapt to the local environments where they are called. However, deep binding is necessary in
situations where maintaining a subroutine's original environment is crucial for its correct operation.
For languages that use a central reference table for dynamic scoping, creating a closure involves
copying parts of the table, potentially saving only the relevant names to reduce overhead. Dynamic
scope languages often offer the option of deep binding, like Lisp, where functions can be passed and
executed in their original environment using closures. In statically scoped languages, deep binding is
typically the default, ensuring subroutines access the correct instances of objects based on their
lexical nesting. This is essential for maintaining the integrity of referencing environments, especially
with recursive calls, as closures capture the current instance of every object at the time of their
creation.
The above Pascal code demonstrates the concept of deep binding using nested procedures and the
passing of subroutines as parameters. The main program begins by calling procedure A with
parameters 1 and C, where C is an empty procedure serving as a placeholder. Procedure A accepts an
integer I and a procedure P. Within A, there is a nested procedure B, which prints the value of I. In the
main body of A, there is a conditional statement that checks if I is greater than 1. If true, it calls
procedure P; otherwise, it recursively calls itself with parameters 2 and B.
During execution, the initial call A(1, C) starts with I equal to 1 and P set to C. Since I is not greater
than 1, the else branch executes, calling A(2, B). Now I is 2 and P is B. Since I is greater than 1, P (which
is B) is called, and procedure B prints the value of I, which is now 2. This demonstrates deep binding,
as the closure created for P ensures that B retains the value of I from the defining environment (when
I was 2). Thus, the code shows how the environment in which a subroutine is created is preserved
and used during its execution, ensuring that variables hold the correct values from their defining
scope.
Most imperative languages treat subroutines as second-class values, but Ada 83 treats them as third-
class. Conversely, functional programming languages, C#, Perl, Python, and some other imperative
languages treat subroutines as first-class values.
First-class subroutines introduce complexity in languages with nested scopes because a subroutine
reference may outlive the scope in which it was declared. For example, in Scheme, a function f created
within a let construct retains access to a variable from its enclosing scope even after that scope has
exited.
This code demonstrates how Scheme handles first-class subroutines and closures:
• Capturing Environment: The lambda function (lambda (y) (+ x y)) captures the value of x
from its defining environment. When f is later called with 3, it correctly computes the sum
using the captured x value 2, demonstrating how closures can outlive their defining scope
while still retaining access to their original environment.
Local objects in functional languages usually have unlimited lifetimes, managed by garbage collection.
In Algol-family languages, they have limited lifetimes, being destroyed at the end of their scope.
This difference affects memory allocation: limited extent objects can use stack allocation, while
unlimited extent objects require heap allocation.
To prevent dangling references in closures, imperative languages with first-class subroutines use
various mechanisms.
C, C++, and Fortran lack nested subroutines, while Modula-2 restricts references to outermost
subroutines.
Modula-3 allows nested subroutines as parameters but restricts their return or storage.
Ada 95 uses a containment rule to ensure subroutine references do not outlive their intended scope.
4. SEPARATE COMPILATION
Separate compilation is a fundamental feature for programming languages that support the
development of large programs. It allows different parts of a program to be compiled independently
and then linked together. This feature is essential for several reasons:
Examples:
i. C Program:
math.h
#ifndef MATH_H
#define MATH_H
int add(int x, int y);
int multiply(int x, int y);
#endif
Math.c
#include "math.h"
int add(int x, int y) {
return x + y;}
Main.c
#include <stdio.h>
#include "math.h"
int main() {
int a = 5, b = 3;
printf("Add: %d\n", add(a, b));
printf("Multiply: %d\n", multiply(a, b));
return 0;
}
The given C program demonstrates the concept of separate compilation. Separate compilation allows
you to split a program into multiple files, which can be compiled independently and then linked
together to create the final executable. This approach improves modularity, maintainability, and
compilation efficiency. The program consists of three files: math.h, math.c, and main.c.
This file contains the declarations of the functions add and multiply. Header files are included in other
files to provide the necessary declarations for the functions they use.
Preprocessor Directives: #ifndef, #define, and #endif are used to prevent multiple inclusions of the
same header file.
Function Declarations: int add(int x, int y); and int multiply(int x, int y); are declarations of the
functions that will be defined in math.c.
This file contains the definitions of the functions declared in math.h. It implements the logic for add
and multiply.
Include Header File: #include "math.h" includes the header file to ensure that the function definitions
match their declarations.
Function Definitions: int add(int x, int y) and int multiply(int x, int y) are defined to return the sum
and product of x and y, respectively.
This file contains the main function that uses the functions add and multiply from math.c.
Include Header Files: #include <stdio.h> includes the standard I/O library, and #include "math.h"
includes the header file with function declarations.
Main Function: The main function declares two integers, a and b, and then prints the results of add(a,
b) and multiply(a, b) using printf
• Separate Compilation: Each source file (math.c and main.c) is compiled independently into object
files (math.o and main.o). These object files are then linked together to create the executable.
• Modularity: By separating the function definitions into math.c and keeping the declarations in
math.h, the code becomes more modular and easier to maintain. Changes to the implementation
in math.c do not require changes in main.c, as long as the interface in math.h remains the same.
• Efficiency: Only the modified files need to be recompiled, reducing the overall compilation time
for large projects.
This approach to programming with separate compilation enhances code organization, reusability,
and collaboration among multiple developers working on different parts of a project.
math.cpp
#include "math.hpp"
int add(int x, int y) {
return x + y;
}
int multiply(int x, int y) {
return x * y;
}
main.cpp
#include <iostream>
#include "math.hpp"
int main() {
int a = 5, b = 3;
std::cout << "Add: " << add(a, b) << std::endl;
std::cout << "Multiply: " << multiply(a, b) << std::endl;
return 0;
}
The given C++ code demonstrates the concept of separate compilation. Separate compilation allows
different parts of a program to be compiled independently and then linked together to form the final
executable. This approach is beneficial for large projects, as it improves modularity, maintainability,
and compilation efficiency. The program consists of three files: math.hpp, math.cpp, and main.cpp.
This file contains the declarations of the functions add and multiply. Header files are included in other
files to provide the necessary declarations for the functions they use.
Include Guard: #ifndef MATH_HPP, #define MATH_HPP, and #endif are preprocessor directives that
prevent multiple inclusions of the same header file, which can lead to redefinition errors.
Function Declarations: int add(int x, int y); and int multiply(int x, int y); are declarations of the
functions that will be defined in math.cpp.
This file contains the definitions of the functions declared in math.hpp. It implements the logic for add
and multiply.
Include Header File: #include "math.hpp" includes the header file to ensure that the function
definitions match their declarations.
Function Definitions: int add(int x, int y) and int multiply(int x, int y) are defined to return the sum
and product of x and y, respectively.
This file contains the main function that uses the functions add and multiply from math.cpp.
Include Header Files: #include <iostream> includes the standard I/O library, and #include "math.hpp"
includes the header file with function declarations.
Main Function: The main function declares two integers, a and b, and then prints the results of add(a,
b) and multiply(a, b) using std::cout.
Separate Compilation: Each source file (math.cpp and main.cpp) is compiled independently into
object files (math.o and main.o). These object files are then linked together to create the executable.
Modularity: By separating the function definitions into math.cpp and keeping the declarations in
math.hpp, the code becomes more modular and easier to maintain. Changes to the implementation in
math.cpp do not require changes in main.cpp, as long as the interface in math.hpp remains the same.
Efficiency: Only the modified files need to be recompiled, reducing the overall compilation time for
large projects.
MainApp.java
public class MainApp {
public static void main(String[] args) {
int a = 5, b = 3;
System.out.println("Add: " + MathOperations.add(a, b));
System.out.println("Multiply: " + MathOperations.multiply(a, b));
}
}
In Java, separate compilation is achieved through the use of classes, which are compiled into
separate .class files. Each class is typically defined in its own .java file, making it easier to manage
large projects. Below is an example demonstrating separate compilation using two classes:
MathOperations and MainApp.
MathOperations.java
This file defines the MathOperations class, which contains two static methods: add and multiply.
Class Definition: The MathOperations class is defined with two static methods.
Method Definitions: The add method returns the sum of x and y, and the multiply method returns their
product.
MainApp.java
This file defines the MainApp class, which contains the main method that uses the MathOperations
class.
Separate Compilation: Each .java file is compiled independently into a .class file.
MathOperations.java compiles to MathOperations.class, and MainApp.java compiles to MainApp.class.
Modularity: The MathOperations class can be used by any other class in the project, promoting code
reuse. The MainApp class uses the methods defined in MathOperations, demonstrating how separate
compilation supports modularity.
Encapsulation: The methods in MathOperations are encapsulated within the class. This separation
allows for better organization and maintainability of the code. Changes to the implementation of
MathOperations do not require changes in MainApp, as long as the interface (method signatures)
remains the same.
5. SUMMARY
In this unit, learners have studied the foundational concepts of scope rules, binding environments,
and separate compilation, which are essential for mastering programming language principles and
improving code quality and also Scope Rules, which are critical for understanding the visibility and
lifetime of variables and functions within a program.
Static Scoping determines the scope of a variable based on the program’s structure, making it easier
to predict and understand variable behavior. This concept is contrasted with Dynamic Scoping, where
the scope of variables is determined at runtime based on the calling sequence of functions, offering
more flexibility but often leading to less predictable behavior. Learners also examined the use of
Blocks, which create new scopes within a program, allowing for the declaration of local variables and
enhancing modularity.
The Declaration Order was another important topic, highlighting how the sequence of variable and
function declarations affects their accessibility and use within a program. Global Scope was discussed
as the scope where variables and functions are accessible throughout the entire program, providing
a means for shared data across different parts of the program.
In the section on Binding of Referencing Environments, learners explored Subroutine Closures, which
capture the environment in which a function is defined, preserving variable bindings for later use.
This concept is particularly important in functional programming languages where functions can be
passed as first-class citizens. Understanding the difference between First- and Second-Class
Subroutines is crucial, as it distinguishes between subroutines that can be passed as arguments or
returned from other subroutines (first-class) and those that cannot (second-class).
Finally, the unit covered Separate Compilation, a technique used to manage large programs by
dividing them into smaller, independently compiled modules. This approach enhances code
modularity, maintainability, and allows for parallel development by different team members.
6. SELF-ASSESSMENT QUESTIONS
Multiple choice Questions
1 Which of the following determines the scope of a variable based on the program’s structure?
A) Dynamic Scoping
B) Static Scoping
C) Block Scoping
D) Global Scoping
2 Which of the following allows the declaration of local variables within a new scope?
A) Global Scope
B) Dynamic Scope
C) Blocks
D) Closures
3 Which scope allows variables and functions to be accessible throughout the entire program?
A) Local Scope
B) Block Scope
C) Global Scope
D) Dynamic Scope
4 What captures the environment in which a function is defined, preserving variable bindings
for later use?
A) Blocks
B) Closures
C) Global Scope
D) Static Scope
5 Which of the following can be passed as arguments or returned from other subroutines?
A) First-Class Subroutines
B) Second-Class Subroutines
C) Third-Class Subroutines
D) Dynamic Subroutines
6 Which programming technique enhances code modularity and maintainability by dividing
programs into smaller units?
A) Static Scoping
B) Dynamic Scoping
C) Separate Compilation
D) Subroutine Closures
7 Which of the following is a benefit of static scoping?
A) Increased flexibility
B) Easier prediction of variable behavior
C) Runtime determination of scope
D) Improved memory usage
Fill in the Blanks:
True or False:
12 Separate compilation helps manage large programs by dividing them into smaller units.
7. GLOSSARY
Financial Management is concerned with the procurement of the least cost funds, and its effective
Sections of code that create a new scope, typically enclosed in curly braces {}
Blocks -
in many programming languages.
Declaration The sequence in which variables and functions are declared, which can affect
-
Order their scope and visibility.
Binding - The association between a name and an entity, such as a variable or a function.
Referencing
- The set of active bindings at a given point in the execution of a program.
Environment
Subroutine Function objects that encapsulate both the function's code and the referencing
-
Closures environment in which it was defined.
First-Class Functions that can be passed as arguments, returned from other functions,
-
Subroutines and assigned to variables.
Second-Class Functions that can be passed as arguments but cannot be returned from other
-
Subroutines functions or assigned to variables.
Separate The process of compiling program modules independently and then linking
-
Compilation them together to form an executable.
Module - A self-contained unit of code that can be separately compiled and linked.
The output of a compiler, containing machine code and data that can be linked
Object File -
to form an executable.
8. TERMINAL QUESTIONS
1. Define static scoping and explain its significance in programming.
2. What are blocks in programming languages and how do they affect scope?
3. Explain the importance of declaration order in a statically scoped language.
4. How does global scope differ from local scope in programming?
5. Describe dynamic scoping with an example.
6. Illustrate the concept of referencing environments and their significance in subroutine binding.
7. Explain subroutine closures with a detailed example in a programming language of your choice.
8. Discuss the differences between first-class and second-class subroutines and their implications.
9. Describe the process of separate compilation and its benefits for large-scale software
development.
10. Explain how separate compilation is handled in C++ with appropriate examples.
9. ANSWERS
Self-Assessment Questions
1. Static Scoping
2. Blocks
3. Global Scope
4. Closures
5. First-Class Subroutines
6. Separate Compilation
7. Easier prediction of variable behaviour
8. Independently
9. Returned
10. False
11. True
12. True
10. REFERENCES
1. Michael L.Scott, “Programming Language Pragmatics”, 2015, Fourth Edition, Morgan Kaufmann
publisher.
2. Robert W. Sebesta, “Concepts of Programming Languages”, 2012, Tenth Edition, . ISBN 10: 0-13-
139531-9.
DCA1209
PRINCIPLE OF PROGRAMMING
LANGUAGES
Unit: 5 - Control Flow: Structured and Unstructured 1
DCA1209: Principle of Programming Languages
Unit – 5
Control Flow: Structured and
Unstructured
TABLE OF CONTENTS
Fig No /
SL SAQ / Page
Topic Table /
No Activity No
Graph
1 Introduction - -
4-5
1.1 Objectives - -
2 Expression Components - -
2.2 Assignments - -
3 Structured and Unstructured Flow - -
17 - 20
3.1 Continuations - -
4 Sequencing - - 21 - 22
5 Selection - -
1. INTRODUCTION
In the previous unit 4, learners have studied how variables and subroutines are accessed within a
program, focusing on scope rules. They learned about static scoping, where variable access is
determined by the structure of the code, and dynamic scoping, which depends on the order of function
calls during execution. The unit also discussed the importance of blocks and declaration order in
controlling variable visibility, as well as the global scope that allows variables to be accessed
throughout the program. Additionally, learners were introduced to subroutine closures and the
difference between first- and second-class subroutines, along with the concept of separate
compilation, which enables parts of a program to be compiled independently.
In this unit, learners will study the fundamental aspects of programming languages that dictate how
expressions are formed, evaluated, and controlled within a program. The unit begins with an
exploration of Expression Components, focusing on the concepts of Precedence and Associativity
which determine the order in which operators are applied in expressions. The learners will explore
the topic of Assignments, where learners will explore into how values are stored and manipulated
within variables.
Then learners examine the Structured and Unstructured Flow, providing insights into the various
ways control flow can be managed in a program, including the use of traditional constructs like loops
and conditionals, as well as the less conventional use of unstructured flow mechanisms.
Next, learners will explore Sequencing, which is central to imperative programming, governing the
order in which instructions are executed. This is followed by an in-depth look at Selection
mechanisms, with particular attention given to Short-Circuited Conditions—a technique used to
optimize the evaluation of Boolean expressions—and the implementation of Case/Switch Statements,
which offer an efficient and readable alternative to multiple nested conditions.
To study this unit, learners have to focus on understanding the logic behind operator precedence and
control flow constructs by practicing with real code examples. Use visual aids like flowcharts to grasp
the concepts of structured and unstructured flow. Finally, learners have to reinforce their learning by
solving problems that involve complex expressions and selection statements, ensuring you apply the
concepts in varied contexts.
1.1. Objectives
After studying this unit, you should be able to:
2. EXPRESSION COMPONENTS
An expression typically consists of either a simple object like a literal constant or a named
variable/constant, or an operator/function applied to a collection of operands or arguments.
The term "operator" is generally used for built-in functions that use special, simple syntax.
The term "operand" refers to the arguments or values that the operator acts upon.
Conditional Expressions:
• Multi-word infix notation is used in some languages like Algol for conditional expressions, e.g.,
a := if b <> 0 then a/b else 0;.
• In C, the equivalent would be a = b != 0 ? a/b : 0;, which is a three-operand infix operator.
Postfix Notation:
• Used in languages like Postscript, Forth, and some assembly languages, postfix notation places
the operator after the operands, e.g., 1 2 + would be the postfix notation for 1 + 2.
• Examples include the pointer dereferencing operator ^ in Pascal and the post-increment and
post-decrement operators ++ and -- in C and its descendants.
The example provided involves Fortran's exponentiation operator (**), showing different ways to
group an expression like a + b * c**d**e/f. The correct grouping according to Fortran is the last one
shown, which demonstrates the significance of operator precedence in determining the order of
operations.
These rules are crucial in resolving ambiguities in expressions, ensuring that the intended operations
are performed in the correct order.
For example, in C and its descendants, the precedence structure is richer, incorporating various
operators such as type casts, array subscripting, and function calls, which may not be considered in
other languages.
Figure 1: Precedence levels of Operators in the Languages like Fortran, Pascal, C and Ada.
2.2. Assignments
In purely functional languages, the building blocks of programs are expressions, and the computation
is entirely based on evaluating these expressions. The result of any expression in such languages is
determined solely by the expression itself and its surrounding context. Functional languages often
use recursion to handle complex computations, potentially creating an infinite number of values and
contexts.
In contrast, imperative languages focus on sequences of commands that change the values stored in
variables. Assignments are the primary method for making these changes. Each assignment operation
involves providing a value and specifying a variable where this value should be stored.
A side effect occurs when a programming language construct affects subsequent computation beyond
just returning a value. Purely functional languages avoid side effects; they are designed so that the
value of an expression depends only on the context in which it is evaluated, not on the timing of that
evaluation. This characteristic is known as referential transparency. In imperative programming,
computations often produce side effects, especially through assignments. Even though an assignment
evaluates to a value, its primary purpose is to change the state of a variable. This change influences
future computations in the program, as the altered variable might be used in subsequent operations.
• Imperative Languages: Imperative languages like C, Java, and Pascal rely heavily on
assignments to perform computation. An assignment involves updating the value of a variable
by associating it with a new value. Each assignment has a side effect because it changes the
program's state by altering the contents of a variable's memory location.
R-values: R-values are the values assigned to L-values. They can appear on the right-hand
side of an assignment. In the previous example, a is an R-value because it provides the value
that will be stored in the location referred to by ‘d’.
Here, a is an L-value because it refers to the memory location that will hold the result of b
+ c. The expression b + c is an R-value because it is a computed value that can be assigned
to a.
For example, C++ allows functions to return references (L-values), not just values (R-values):
g(a).b[c] = 2;
In this expression, g(a) returns a reference to a structure. The reference allows direct
assignment to the structure's member b[c].
Each variable has an independent value. Assigning a = b would copy the value of b into a, but
the two variables remain independent.
Reference Model: In the reference model, used in languages like Clu or Java, variables hold
references to values rather than the values themselves. This means that multiple variables can
refer to the same underlying object in memory.
For instance:
b := 2;
c := b;
a := b + c;
Under the reference model, b and c could refer to the exact memory location. Changing b would
automatically change c because they are references to the same value.
Figure 2: The reference (right) and value (left) forms of a variable's model.
ii. Boxing
In Java Boxing, there is a limitation to passing built-in types uniformly to methods that expect
class-typed parameters when utilising a value model. For instance, while using earlier Java
versions, it was necessary to "wrap" objects of built-in types in corresponding predefined class
types before inserting them into standard container (collection) classes.
In this code, the integer value 13 is manually wrapped into an Integer object using new
Integer(13). This object can then be stored in the HashTable or other collections that require
objects rather than primitive types. The reverse process, known as unboxing, converts the Integer
object back into a primitive int.
With the release of Java 5, automatic boxing and unboxing were introduced. The compiler now
automatically wraps primitives into their respective object types and vice versa, removing the
need for manual wrapping.
For example:
ht.put(13, 31);
int m = (Integer) ht.get(13);
In this example, 13 and 31 are automatically boxed into Integer objects when added to the
HashTable. Unboxing occurs when the value from the HashTable is retrieved and assigned to
variable m.
In C# Boxing, C# takes boxing a step further by allowing not just the arguments but also the cast
to be "boxed." This eliminates the need for manual conversion in most cases.
For example:
ht[13] = 31;
int m = (int) ht[13];
In this C# code snippet, ht[13] = 31; uses boxing implicitly to store the integer 31 as an object. The
second line retrieves the value, unboxing it back into an integer.
C# also supports indexers, which allow objects to be accessed using array-like syntax (ht[13]).
This is a powerful feature that allows for more intuitive manipulation of objects that behave like
arrays.
iii. Orthogonality
One of the main design principles of Algol 68 was orthogonality, which aimed to make the features
of the language consistent and combinable in any meaningful way. This means that different
features of the language work together seamlessly, much like orthogonal vectors in linear algebra
that combine without dependency.
Algol 68 stands out for its expression-oriented nature, lacking a separate notion of statements.
Expressions and constructs, which would typically be statements in other languages, can appear
in contexts where expressions are expected.
begin
a := if b < c then d else e;
a := begin f(b); g(c) end;
g(d);
2+3
end
Here, the value of if b < c then d else e depends on the condition and is assigned to a. The begin ...
end block returns the value of the last expression within it. The whole block sums 2 + 3 to get the
value 5.
C language adopts a middle ground, distinguishing between statements and expressions but
allowing expression statements where the value of an expression is computed and discarded. This
allows expressions in places where other languages might restrict to statements.
Algol 68 and C allow assignments within expressions, where the value of an assignment statement
is its right-hand side value.
if (a == b) {
/* do the following if a equals b */
}
if (a = b) {
/* assign b into a and then do
the following if the result is nonzero */
}
The first construct is a proper equality check (==), while the second is an assignment (=) that
evaluates to true if b is non-zero. This can lead to bugs, especially for programmers coming from
languages like Ada, where = is used for equality.
C lacks a separate Boolean type, treating zero as false and any non-zero value as true. C99
introduced a _Bool type, but it's essentially an integer. This makes the second form valid but
potentially erroneous if the programmer intended to check equality.
In contrast, C++ introduced a true Boolean type (bool), although it still shares this problem with
C. Java and C# eliminate this issue by enforcing Boolean expressions in conditional statements,
generating compile-time errors for non-Boolean assignments in conditions.
For example:
• a = a + 1;
• b.c[3].d = b.c[3].d * e;
These expressions can be heavy to write and read, as they involve repeating the variable on both
sides of the assignment. Moreover, such redundancy can lead to inefficiencies, especially if
calculating the variable's address has side effects.
If a function or operation used to determine the address of a variable has side effects, the address
calculation should not be repeated.
Here, index_fn(i) is called once to avoid repeated address calculations, which might have side
effects or cause inefficiencies if called multiple times.
To simplify the code and avoid such issues, many languages provide assignment operators to
update variables more succinctly. These include:
• a += 1; (equivalent to a = a + 1;)
• b.c[3].d *= e; (equivalent to b.c[3].d = b.c[3].d * e;)
These operators not only make the code more readable but also ensure that address calculations
are performed only once, preventing redundant operations.
C and its descendants also provide prefix (++x, --x) and postfix (x++, x--) increment and decrement
operators, which allow further simplification of code that updates variables based on their current
values.
For example:
A[index_fn(i)]++; (or)
++A[index_fn(i)];
These forms are aesthetically pleasing and efficient, combining the update with the address
calculation in a single operation.
The prefix forms of ++ and -- can be seen as syntactic sugar for the more verbose += and -=
operators. However, the postfix forms are not merely syntactic sugar—they provide a distinct
operation that is more complex to replicate with other operators.
To achieve the same result without using postfix, one would need additional variables and code,
making the postfix operators convenient and crucial for writing efficient code.
The discussion also touches on how these operators interact with pointers in C. When applied to
pointers, these operators adjust the pointer's position in memory by the appropriate amount,
depending on the size of the data type it points to. For instance:
This expression simultaneously increments pointers p and q and assigns the values they point to
before the increment.
v. Multiway Assignment
In many programming languages, such as Clu, ML, Perl, Python, and Ruby, multiway assignment
enables a programmer to assign values to multiple variables in a single expression. This is
particularly useful when you want to swap values or return multiple values from a function
without needing auxiliary variables.
a, b := c, d;
Here, the comma (,) on the right-hand side is not the C-style sequencing operator but instead
represents a tuple of multiple r-values (right-hand values). The comma on the left-hand side
similarly represents a tuple of l-values (left-hand values). The effect of this assignment is to copy
the value of c into a and the value of d into b.
a := c;
b := d;
the multiway assignment allows for a more concise and elegant solution.
a, b := b, a;
swaps the values of a and b without needing a temporary variable. This is a common pattern in
many programming languages, facilitating easier and more readable code for such operations.
Additionally, multiway assignments allow functions to return tuples, enabling the assignment of
multiple return values to variables in a single statement.
For instance:
a, b, c := foo(d, e, f);
This assigns the values returned by the function foo to the variables a, b, and c, all in one operation.
In languages like ML, the concept of multiway assignment is further generalized into a powerful
pattern-matching mechanism, which allows more complex and meaningful decompositions of
data structures during assignment. This mechanism enhances the capability of handling data
structures and extracting values within the programming language, promoting more expressive
and compact code.
Here, goto is used to jump to label 10 if A is less than B. The label 10 is a statement label that marks
the point in the code to jump to.
This example shows how goto is used to exit a loop prematurely when a condition (all_blanks(line))
is met.
The goto here skips the rest of the subroutine if a specific condition is met, bypassing the remainder
of the logic.
This example shows how goto is used to return a value from deep within nested subroutines by
escaping the nested context when a condition (found(key, line)) is met.
searchFile("f3", key)
"not found\n"
end
print match
Here, throw is used in Ruby to exit from the normal control flow when a pattern is found in a file. The
catch block handles the thrown exception and allows for a structured exit with a value.
This demonstrates a more basic form of error handling using status codes, where the function
my_proc returns a status that is checked before proceeding.
3.1. Continuations
Continuations refer to a feature in some programming languages that allows a program to capture its
current execution state (context) and later restore it, essentially "pausing" and "resuming" the
execution at a particular point.
• Local Variables: Since local variables in these languages have unlimited extent, they are
typically allocated on the heap (not the stack). This means when a continuation is invoked, the
local variables and the execution state are already available without needing explicit
deallocation or restoration.
• Garbage Collection: When jumping through a continuation, frames that are no longer needed
can be reclaimed by garbage collection, making continuations memory efficient.
• Activation Records: Since the continuation closure retains everything required to resume the
execution, no manual state restoration (like saved registers) is necessary.
While continuations are extremely powerful and versatile, enabling the creation of complex control
structures like iterators, exceptions, and more, they can also lead to highly complex and difficult-to-
understand code if not used carefully. Their flexibility can allow programmers to create inscrutable
logic that is hard to debug or maintain.
4. SEQUENCING
Sequencing is a fundamental concept in imperative programming, determining the order in which
side effects, such as variable assignments, occur. In imperative languages, a statement typically
follows another, executing in the sequence they are written. This order of execution is crucial because
it directly affects the program's state.
A compound statement, often enclosed within begin...end in languages like Algol 68 or {...} in C, groups
multiple statements to be executed as a unit. Such grouping is known as a block. The value of a
compound statement in these languages is typically the value of its final element, making sequencing
not just about order but also about value production.
In languages like Common Lisp, the value returned by a sequence of expressions can be the first,
second, or last expression's value, depending on the context. This is unique compared to purely
imperative languages, where sequencing is more rigid.
The side effects revolve around the impact of certain operations on the state of a program, particularly
within functions. Side-effect freedom refers to functions that produce the same output given the
same input, without modifying any external state. Such functions are idempotent, meaning repeated
calls with the same arguments will yield the same result without altering the program state.
However, not all functions can or should be side-effect-free. For instance, a side effect is essential in a
pseudo-random number generator because the function must return a different value each time it is
called. This necessitates changing the internal state (e.g., updating a seed value).
For example:
Where,
• srand(seed : integer) initialises a random number generator with a seed, ensuring that the
sequence of random numbers is reproducible.
• rand(): integer generates a new random number each time it is called, which inherently
requires a side effect to produce varying outputs.
Ada, a programming language known for its robustness, offers a compromise. While it allows
functions to have side effects, such as changing static or global variables, it prohibits a function from
directly modifying its parameters. This restriction ensures a level of predictability and safety,
particularly in complex systems where unintentional parameter modification could lead to bugs or
unpredictable behaviour.
5. SELECTION
The selection statements are explained with the common if...then...else notation introduced in Algol
60. This notation allows the program to execute different blocks of code based on conditions:
In this structure:
• The if clause checks a condition, and if it’s true, the corresponding then clause is executed.
• The else if clauses allow for additional conditions to be checked sequentially.
• The else clause is executed if none of the previous conditions are met.
Algol 60 and Pascal require both the then clause and the else clause to contain a single statement,
which could be a compound statement enclosed in begin...end. This helps avoid grammatical
ambiguity in the code. For example, Algol 60 requires that the statement after the then keyword begin
with something other than if, preferring to begin with begin to avoid confusion. Pascal, on the other
hand, uses a disambiguating rule that associates an else with the closest unmatched then.
Modula-2 handle nested if statements with terminators, using a special elsif or elif keyword to prevent
a buildup of terminators:
IF a = b THEN ...
ELSIF a = c THEN ...
ELSIF a = d THEN ...
ELSE ...
END
This structure simplifies handling multiple conditions in a clear and organized way.
Cond in Lisp:
The equivalent construct in Lisp is shown using the cond function:
(cond
((= A B)
(...))
((= A C)
(...))
((= A D)
(...))
(T
(...)))
Jump Code: Jump code is used for expressions that can be short-circuited, meaning the evaluation
can stop as soon as the result is determined. For example, in a conditional expression like if (A > B)
and (C > D) or (E ≠ F), evaluation can stop once the first part of the condition is determined to be
false. This approach avoids unnecessary evaluations and can significantly improve efficiency.
In Pascal, which does not use short-circuit evaluation, the code would load and compare each part of
the condition sequentially:
r1 := A
r2 := B
r1 := r1 > r2
r2 := C
r3 := D
r2 := r2 > r3
r1 := r1 & r2
r2 := E
r3 := F
r2 := r2 ≠ r3
r1 := r1 or r2
if r1 = 0 goto L2
L1: then_clause
goto L3
L2: else_clause
L3:
In jump code, inherited attributes of the condition’s root would indicate that control should fall
through to the then_clause if the condition is true or branch to else_clause if it is false:
r1 := A
r2 := B
if r1 <= r2 goto L4
r1 := C
r2 := D
if r1 <= r2 goto L2
L4: r1 := E
r2 := F
if r1 ≠ r2 goto L1
L2: else_clause
goto L3
L1: then_clause
L3:
Short-circuit evaluation allows for more efficient code by stopping the evaluation of an expression as
soon as the result is determined. This feature is beneficial in programming languages and is
commonly used in conditional statements, local scopes for loop indices, and parameter modes in
various languages like Pascal and Ada. The design and implementation section emphasize that short-
circuit evaluation is both semantically useful and faster than alternatives, providing a significant
advantage in programming.
The below example shows a sequence of if, elsif, and else conditions that check the value of i and
execute different blocks of code (clause_A, clause_B, etc.) based on the condition that is met. This can
be written more concisely using a case statement, which directly maps the possible values of i to the
respective code blocks.
clause_A
ELSIF i IN 2, 7 THEN
clause_B
ELSIF i IN 3..5 THEN
clause_C
ELSIF (i = 10) THEN
clause_D
ELSE
clause_E
END
When translating high-level control flow statements like if-else or case statements into low-level code,
jump tables can be used to optimize the execution. The images illustrate how the if-else chain can be
converted into a sequence of goto statements that correspond to different labels (L1, L2, etc.). A jump
table is essentially an array of pointers to these labels, allowing the program to quickly jump to the
correct block of code based on the value of the expression being evaluated.
This shows how the value of r1 is compared, and depending on the result, the code branches to the
corresponding clause, or falls through to the next condition. The jump table at label T maps the
possible values of r1 directly to their respective code labels, which significantly speeds up the decision
process.
The jump table concept involves indexing directly into an array of addresses (labelled &L1, &L2, etc.)
that correspond to the case arms. The table ensures that the execution jumps to the correct block of
code based on the calculated expression. For instance, if r1 is 3, the code might jump to the address
corresponding to &L3.
This method reduces the need for multiple conditional checks and makes the case handling more
efficient, especially in compiled languages where execution speed is critical.
i. Alternative Implementations
Alternative Implementations of Case Statements are:
• Linear Jump Table: This is an efficient method when the case statement labels are densely
packed and do not contain large ranges. It uses a table where each entry corresponds to a case
label, pointing directly to the code block associated with that label. This method can consume
a lot of memory if the case labels are sparse or if there are large gaps between values.
• Sequential Testing (Linear Search): This method is similar to a chain of if...then...else
statements. It has a time complexity of O(n), where n is the number of labels. This method is
simple but not efficient for a large number of case labels, especially if the labels are widely
spread out.
• Hash Function: A hash function can be used to compute the address to jump to, making the
execution faster. However, this method is not ideal when there are large gaps in the label
values or when labels are spread across a wide range.
however, allows an optional else clause, and Ada requires that each label's range of values is
explicitly covered, with no default or else case allowed if a value is missing.
• Binary Search: This method involves splitting the labels into two halves and repeatedly
narrowing down the search range until the correct label is found. It is more efficient than
sequential testing, with a time complexity of O(log n), making it suitable for cases with large
label ranges.
Example:
switch (... /* tested expression */) {
case 1:
clause_A
break;
case 2:
case 7:
clause_B
break;
case 3:
case 4:
case 5:
clause_C
break;
case 10:
clause_D
break;
default:
clause_E
break;
}
This C-style switch statement allows the program to jump to different parts of the code based on
the value of the expression. The break statement is crucial because it prevents the execution from
"falling through" to the next case label unintentionally, which can lead to bugs if not handled
properly.
Fall-through Behavior:
The concept of "fall-through" is explicitly shown in the case where a single label (like case 'A') does
not end with a break statement, allowing the execution to proceed to the next case (case 'a'). While
this behavior can be useful, it often leads to difficult-to-diagnose bugs, so modern languages like C#
require every case to end with a break, goto, continue, or return statement.
Historical Origins:
The case statement evolved from the computed goto statements in early programming languages like
Fortran and Algol 60. These constructs allowed programmers to perform multiway branching based
on integer values, but they were less structured and more prone to errors.
For example:
goto (15, 100, 150, 200), I
This statement directs the program to jump to different labeled points based on the value of I. This
form of branching was eventually replaced by more readable and maintainable case or switch
statements.
6. SUMMARY
In this unit, learners have studied the foundational aspects of expressions and control flow in
programming languages. They explored the importance of precedence and associativity in
determining the order of operations within expressions, ensuring that complex expressions are
evaluated correctly.
Learners also explored the assignments, examining how values are stored and manipulated in
variables through various types of assignment operators.
Learners have learned the differences between structured and unstructured flow, understanding the
benefits and drawbacks of each approach in programming. The concept of sequencing was covered
to illustrate how the order of operations affects the flow of a program, while selection statements
such as if-else and case/switch statements were analyzed to demonstrate how decisions are made in
code.
Learners also emphasized the role of short-circuited conditions, which allow for more efficient
evaluations of logical expressions by avoiding unnecessary calculations.
7. SELF-ASSESSMENT QUESTIONS
Multiple choice Questions
1 Which of the following determines the order in which operations are performed in an
expression?
A) Syntax
B) Precedence
C) Compilation
D) Debugging
2 In most programming languages, which operator usually has the highest precedence?
A) Addition (+)
B) Multiplication (*)
C) Parentheses (())
D) Assignment (=)
3 What does associativity determine in an expression?
A) The order of operations
B) How operators of the same precedence are grouped
C) The scope of variables
D) The type of variable
4 In which direction does the assignment operator typically associate?
A) Left to right
B) Right to left
C) Top to bottom
D) None of the above
5 Which of the following is a characteristic of unstructured flow?
A) Use of loops
B) Use of functions
C) Use of goto statements
D) Use of if-else statements
6 Which of the following best describes sequencing in programming?
A) Repeating a block of code
B) Executing code statements one after another
8. GLOSSARY
Financial Management is concerned with the procurement of the least cost funds, and its effective
A control flow statement that allows the program to jump to another part
Goto Statement -
of the code unconditionally, often leading to unstructured flow.
Short-Circuit A feature in logical operations where the evaluation stops as soon as the
Evaluation result is determined, without evaluating the rest of the expression.
9. TERMINAL QUESTIONS
1. Explain the concept of operator precedence and associativity. How do they influence the
evaluation of expressions in programming languages?
2. Discuss the differences between structured and unstructured flow in programming. Why is
structured programming generally preferred?
3. What is the role of the assignment operator in programming? Provide examples of different types
of assignments.
4. Describe the importance of sequencing in imperative programming. How does it affect program
execution?
5. What are short-circuited conditions? How do they optimize the evaluation of Boolean expressions?
6. Compare and contrast the use of if-else statements and switch-case statements in programming.
Under what circumstances is one preferred over the other?
7. Discuss the concept of assignment in programming languages. How do different languages handle
assignments, and what are the implications of these differences?
8. Explain the concept of jump tables in the context of switch statements. How do jump tables
contribute to the efficiency of switch-case constructs?
9. Analyze the role of short-circuited conditions in optimizing program performance. Provide
examples to illustrate how short-circuited evaluation works in different programming languages.
10. Explore the importance of expression components in programming.
10. ANSWERS
Self-Assessment Questions
1. Precedence
2. Parentheses (())
3. How operators of the same precedence are grouped
4. Right to left
5. Use of goto statements
6. Executing code statements one after another
7. Selection
8. Only the first part of the condition is evaluated if it determines the outcome.
9. The next case statement is automatically executed (fall-through).
10. Case/Switch statement
11. REFERENCES
1. Michael L.Scott, “Programming Language Pragmatics”, 2015, Fourth Edition, Morgan Kaufmann
publisher.
2. Robert W. Sebesta, “Concepts of Programming Languages”, 2012, Tenth Edition, . ISBN 10: 0-13-
139531-9.