CSC 414-514 Notes
CSC 414-514 Notes
CSC 414-514 Notes
COURSE CONTENTS
Syntax:
Definition: Syntax refers to the set of rules that dictate how programs should be written in a particular
language.
Importance: Proper syntax ensures that code is written in a way that the compiler or interpreter can
understand. It contributes to code readability and helps prevent errors.
Semantics:
Definition: Semantics deals with the meaning of code and how statements are executed.
Importance: Understanding semantics is essential for writing code that behaves as intended. It involves
grasping concepts like variables, data types, and control flow to create logic and functionality.
Data Types:
Definition: Data types define the kind of data that can be stored and manipulated in a program, such as
integers, floating-point numbers, strings, and more.
Importance: Proper use of data types ensures data integrity and efficient memory usage. It also
influences the operations that can be performed on the data.
Variables:
Definition: Variables are symbolic names for storage locations in a program where values can be stored
and retrieved.
Importance: Variables allow developers to manage and manipulate data dynamically during program
execution. Understanding variable scope and lifetime is crucial for effective programming.
Control Structures:
Definition: Control structures, including loops and conditional statements, manage the flow of execution
in a program.
Importance: These structures determine the order in which statements are executed. Well-designed
control flow enhances code readability and helps in building complex algorithms.
Functions/Methods:
Definition: Functions or methods are blocks of reusable code that perform a specific task.
Importance: Modularizing code through functions promotes code reusability, readability, and
maintainability. It also facilitates the division of complex tasks into manageable units.
Object-Oriented Programming (OOP) Concepts:
Definition: OOP is a programming paradigm that uses objects, encapsulation, inheritance, and
polymorphism to organize code.
Importance: OOP fosters code organization, abstraction, and reuse. It enhances software design by
modeling real-world entities and their relationships.
Memory Management:
Definition: Memory management involves allocating and deallocating memory during program
execution.
Importance: Efficient memory management prevents memory leaks and enhances program
performance. Understanding concepts like manual memory allocation and garbage collection is crucial.
Error Handling:
Definition: Error handling involves dealing with unexpected situations and errors that may occur during
program execution.
Importance: Robust error handling improves the reliability of software. It includes techniques like
exception handling to gracefully manage errors without crashing the program.
Definition: These concepts deal with executing multiple tasks simultaneously, either concurrently or in
parallel.
Importance: Understanding concurrency and parallelism is crucial for developing efficient and
responsive software, particularly in applications that require multitasking or utilize multiple cores.
Syntax refers to the set of rules governing the structure of sentences or expressions in a language. In the
context of computer science and programming languages, syntax definition plays a crucial role in
specifying the valid combinations of symbols that constitute well-formed programs. It involves defining
the grammar of a language, outlining how statements and expressions should be structured.
Lexical Elements: Define the basic building blocks such as identifiers, keywords, operators, and literals.
Grammar Rules: Specify how lexical elements can be combined to form valid statements or expressions.
Context-free grammars (CFGs) are commonly used for this purpose.
Syntax Diagrams or BNF Notation: Illustrate the grammar rules using diagrams or Backus-Naur Form
(BNF) notation, providing a visual representation of the language's structure.
2. Parsing Techniques:
Parsing is the process of analyzing a sequence of symbols to determine its grammatical structure with
respect to a given grammar. It is a crucial step in the compilation process of programming languages.
Various parsing techniques are employed to achieve this goal:
a. Top-Down Parsing:
Recursive Descent Parsing: In this approach, the parser starts with the top-level grammar rule and
recursively expands non-terminals until the input is recognized. Each non-terminal corresponds to a
specific syntactic construct.
LL Parsing: LL parsers predict the production rule to be applied based on the next input symbol. LL(k)
parsers use k lookahead symbols to make parsing decisions.
b. Bottom-Up Parsing:
LR Parsing: LR parsers, such as LR(1) or LALR(1), build a parse tree in a bottom-up manner, starting with
individual tokens and reducing them to higher-level constructs. LR parsers handle a broader class of
grammars compared to LL parsers.
Shift-Reduce Parsing: In shift-reduce parsing, the parser repeatedly shifts input symbols onto a stack or
reduces them using production rules until the entire input is reduced to the start symbol.
parsing examples using a simple context-free grammar and two common parsing techniques: Recursive
Descent Parsing and LR Parsing.
Example Grammar:
E→E+T|T
T→T*F|F
F → ( E ) | id
This grammar describes expressions involving addition (+), multiplication (*), parentheses, and
identifiers (id).
Recursive Descent Parsing involves constructing a set of recursive procedures to match the grammar
rules. Let's create a recursive descent parser for the given grammar in Python:
python
def parse_E(tokens):
if tokens[0] == 'id':
return parse_T(tokens[1:])
else:
return parse_T(tokens)
def parse_T(tokens):
if tokens[0] == 'id':
return parse_F(tokens[1:])
else:
return parse_F(tokens)
def parse_F(tokens):
if tokens[0] == '(':
return parse_E(tokens[1:])
elif tokens[0] == 'id':
return tokens[1:]
else:
raise SyntaxError("Invalid expression")
# Example usage:
expression = ["id", "+", "id", "*", "id"]
result = parse_E(expression)
print(result)
This recursive descent parser recursively calls functions corresponding to grammar rules to parse the
input expression.
2. LR Parsing:
Let's use an LR parser to parse the same grammar. We'll use a tool like Yacc (or Bison) to generate the
parser: Yacc stands for "Yet Another Compiler Compiler." It is a tool used in the construction of
compilers and interpreters for programming languages. Yacc is a code generator that generates parsers
based on formal grammars specified in a specific syntax. Yacc is often used in combination with Lex (a
lexical analyzer generator) to create a complete language processing system.
The Yacc tool reads a formal grammar description and generates a parser in C (or other target
languages) that can recognize and parse syntactic structures defined by the grammar. The generated
parser typically produces a parse tree or an abstract syntax tree, which can be further processed to
execute or translate the input language.
yacc
%token id PLUS TIMES LPAREN RPAREN
%%
expression: E
E: E PLUS T | T
T: T TIMES F | F
F: LPAREN E RPAREN | id
This Yacc specification defines tokens (id, PLUS, TIMES, LPAREN, RPAREN) and production rules for the
grammar. The generated parser uses LR parsing.
Parsing Process:
3. Language Translation:
- Compilers vs. interpreters.
- Lexical analysis and parsing.
- Intermediate code generation.
4. Language Implementation:
- Memory management and allocation.
- Symbol tables and symbol management.
- Scope and lifetime of variables.
5. Data Types:
Programming languages provide a range of data types to represent different kinds of information. The
organization of data types plays a crucial role in defining how data is stored, manipulated, and operated
upon in a program.
Records (Structures):
Records or structures allow grouping different data types under a single name.
Example (C#):
struct Person {
string name;
int age;
}
Unions:
Unions allow a variable to hold different data types at different times.
Example (C):
union Data {
int i;
float f;
char c;
};
# ...
Classes (Object-Oriented Programming): Classes define blueprints for creating objects with attributes
(data) and methods (functions).
Example (Java):
class Dog {
String breed;
int age;
void bark() {
System.out.println("Woof!");
}
}
Abstract Data Types (ADTs): ADTs represent a mathematical model of a data structure along with a set
of operations to manipulate it.
Example (C++ - Standard Template Library):
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
}
Pointers: Pointers store memory addresses, allowing dynamic memory allocation and manipulation.
Example (C):
int* ptr = malloc(sizeof(int));
*ptr = 10;
If-Else Statement: The if-else statement allows you to execute one block of code if the condition is true
and another block if it is false.
Example (JavaScript):
let temperature = 25;
if (temperature > 30) {
console.log("It's hot!");
} else {
console.log("It's not too hot.");
}
If-Elif-Else Statement: The if-elif-else statement allows you to check multiple conditions in sequence.
Example (C++):
int score = 85;
if (score >= 90) {
cout << "A";
} else if (score >= 80) {
out << "B";
} else {
cout << "C";
}
Nested Ternary Operators: Ternary operators can be nested to handle more complex conditions.
Example (Python):
num = 7
classification = "Even" if num % 2 == 0 else ("Odd" if num % 2 != 0 else "Invalid")
Detailed Examples:
Example 1: Python If-Else Statement
# Check if a number is positive or negative
num = -8
if num > 0:
print("Positive number")
else:
print("Negative or zero")
For Loop: Definition: The for loop is used for iterating over a sequence (such as a range of numbers) and
executing a block of code for each iteration.
Example (Python):
for i in range(5):
print(i)
While Loop: Definition: The while loop repeatedly executes a block of code as long as a specified
condition is true.
Example (JavaScript):
let i = 0;
while (i < 5) {
console.log(i);
i++;
}
Foreach Loop: The foreach loop (or for-each loop) is used for iterating over elements in a collection,
such as an array.
Example (Java):
int[] numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
System.out.println(num);
}
Detailed Examples:
Example 1: Python For Loop
# Print square of numbers from 1 to 5
for i in range(1, 6):
square = i ** 2
print(f"The square of {i} is {square}")
Continue Statement: The continue statement is used to skip the rest of the code inside a loop for the
current iteration.
Example (Python):
for i in range(10):
if i % 2 == 0:
continue
print(i)
Nested Loops:
Nested For Loops: Nested loops are loops inside another loop. They are used for tasks that involve
multiple levels of iteration.
Example (C#):
for (int i = 1; i <= 3; ++i) {
for (int j = 1; j <= 3; ++j) {
Console.WriteLine($"({i}, {j})");
}
}
Nested While Loops: Similar to nested for loops, while loops can also be nested to achieve complex
iteration patterns.
Example (JavaScript):
let i = 1;
while (i <= 3) {
let j = 1;
while (j <= 3) {
console.log(`(${i}, ${j})`);
j++;
}
i++;
}
Subroutine: The term "subroutine" is a generalization that includes both functions and procedures.
Procedures are subroutines that perform a task without returning a value.
Example (C):
void print_message() {
printf("Hello, subroutine!\n");
}
Function Calls:
Function Call: A function call invokes the execution of a specific function with given arguments. The
result (return value) can be stored or used in further computations.
Example (JavaScript):
let result = addNumbers(3, 4);
console.log(result); // Output: 7
Function Parameters: Functions can accept parameters, which are values passed to the function when it
is called. These parameters act as inputs for the function.
Example (Java):
int multiply(int x, int y) {
return x * y;
}
Default Parameters: Some languages allow functions to have default parameter values, providing a
fallback if a value is not explicitly provided during the function call.
Example (Python):
def greet(name="Guest"):
return f"Hello, {name}!"
Subroutine Calls: A subroutine call (procedure call) invokes the execution of a specific subroutine. Unlike
functions, procedures typically do not return a value.
Example (Fortran):
CALL PrintMessage()
Passing by Reference: Subroutines can modify the values of variables passed to them by reference,
allowing for side effects.
void modifyValue(int &x) {
x *= 2;
}
Detailed Examples:
Example 1: Python Function Call
# Calculate the area of a rectangle using a function
def calculate_area(length, width):
return length * width
# Function call
area = calculate_area(5, 8)
print("Area:", area)
// Function call
let message = greet("John");
console.log(message);
7. Parameter Passing:
- Call by value, call by reference, and call by sharing.
Parameter passing defines how arguments are transferred to functions or methods. The three common
methods of parameter passing are Call by Value, Call by Reference, and Call by Sharing.
1. Call by Value:
In Call by Value, the actual value of the argument is passed to the function. This means that the function
receives a copy of the argument, and any modifications made to the parameter inside the function do
not affect the original value outside the function. It is a simple and safe method, commonly used in
languages like C and Python for primitive data types.
def square(number):
number = number ** 2
return number
x=5
result = square(x)
print(x) # Output: 5
print(result) # Output: 25
2. Call by Reference:
In Call by Reference, the memory address (reference) of the argument is passed to the function. This
allows the function to directly access and modify the original data. Call by Reference is often found in
languages like C++ and can lead to unintended side effects if not used carefully.
#include <iostream>
void increment(int &value) {
value++;
}
int main() {
int x = 5;
increment(x);
cout << x << endl; // Output: 6
return 0;
}
result = factorial(5)
print(result) # Output: 120
2. Fibonacci Sequence:
def fibonacci(n):
# Base case
if n <= 1:
return n
# Recursive case
else:
return fibonacci(n - 1) + fibonacci(n - 2)
result = fibonacci(6)
print(result) # Output: 8
3. Binary Search
def binary_search(arr, target, low, high):
# Base case
if low > high:
return -1
mid = (low + high) // 2
# Base case: element found
if arr[mid] == target:
return mid
# Recursive cases
elif arr[mid] > target:
return binary_search(arr, target, low, mid - 1)
else:
return binary_search(arr, target, mid + 1, high)
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
target = 5
result = binary_search(arr, target, 0, len(arr) - 1)
print(result) # Output: 4
3. Call by Sharing:
Call by Sharing is a concept associated with languages like Java and Python. In this method, the
reference to the object is passed to the function, but the function cannot modify the actual object.
However, if the object is mutable (e.g., a list or dictionary), changes made to its contents are reflected
outside the function.
def modify_list(my_list):
my_list.append(4)
my_list = [10, 20, 30]
numbers = [1, 2, 3]
modify_list(numbers)
print(numbers) # Output: [1, 2, 3, 4]
8. Exception Handling:
- Error handling mechanisms.
Error handling is a crucial aspect of programming that involves detecting, responding to, and resolving
errors or exceptional situations in a program. Proper error handling enhances the reliability and
robustness of code. Some common error handling mechanisms include:
2. Return Codes:
In languages like C or C++, functions often use return codes to indicate success or failure.
Example
#include <stdio.h>
int main() {
int result;
if (divide(10, 2, &result) == 0) {
printf("Result: %d\n", result);
} else {
printf("Error: Division by zero\n");
}
return 0;
}
4. Assert Statements:
Assert statements are used to check assumptions during development and can be disabled in production
for better performance.
Example (Python):
def divide(a, b):
assert b != 0, "Division by zero"
return a / b
5. Logging:
Logging is a way to record information about the program's execution, including errors.
Example (Python):
import logging
try:
result = 10 / 0
except Exception as e:
# Log the error
logging.error(f"An error occurred: {e}", exc_info=True)
6. Custom Exceptions:
Defining custom exceptions can be useful for handling specific error scenarios.
Example (Python):
class CustomError(Exception):
def __init__(self, message):
self.message = message
def process_data(data):
if not data:
raise CustomError("Data is empty")
try:
process_data([])
except CustomError as e:
print(f"Custom error: {e.message}")
Closures Example:
; Define a function that returns a closure
(defun make-counter ()
(let ((count 0))
(lambda ()
(setq count (+ count 1))
count)))
Examples:
1. Write a lambda expression in Lisp that squares a given number.
(setq square (lambda (x) (* x x)))
2. Write a lambda expression that takes two arguments and returns the product of their squares.
(setq product-of-squares (lambda (x y) (* (* x x) (* y y))))
4. Create a function map-squared that takes a list of numbers and returns a new list containing the
squares of each number.
(defun map-squared (lst)
(mapcar (lambda (x) (* x x)) lst))
5. Define a function sum-recursive that uses recursion to calculate the sum of all elements in a list.
(defun sum-recursive (lst)
(if (null lst)
0
(+ (car lst) (sum-recursive (cdr lst)))))
Rules:
In Prolog, rules define relationships and conditions within a problem domain. A rule consists of a head
and a body, separated by the :- (if) symbol. The head specifies a goal or conclusion, and the body
contains conditions that must be satisfied for the rule to be true. For example:
parent(john, mary).
parent(john, jim).
In this example, the rules state that John is the parent of Mary and John is also the parent of Jim.
Queries:
Queries in Prolog are used to retrieve information or test relationships based on the defined rules. A
query consists of a goal or a set of goals that the Prolog interpreter attempts to satisfy using the given
rules. For instance:
?- parent(john, mary).
Examples:
1. Define a rule in Prolog representing the "sibling" relationship.
% Define the "parent" relationship
parent(john, mary).
parent(john, jim).
parent(susan, mary).
parent(susan, jim).
parent(bob, ann).
If the knowledge base and rules are loaded into a Prolog interpreter, running this query might yield
results like:
Sibling = jim
def bark(self):
print(f"{self.name} is barking!")
2. Inheritance:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
3. Polymorphism:
class Shape:
def area(self):
pass
class Square(Shape):
def __init__(self, side_length):
self.side_length = side_length
def area(self):
return self.side_length ** 2
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
(Skip)