I was tasked with creating a parser that performs semantic analysis for MiniJava. For this I made two visitors, one which creates the symbol table and one which performs type checking.
I seperated the symbol table implementation in three files, Scopes which has a map with all symbol tables linked with the function name or class name, ST which implements the symbol table with a map that connects variable and method names with information about itself and Info which has all the information used to perform type checking.
Manages scoped symbol tables for a MiniJava-like compiler. Tracks class-level scopes, handles variable/method declarations, and supports inheritance-aware type and method checks.
- Create/switch scopes (
enter,exit,enterScopeByName) - Insert variables and methods (
insert) - Lookup symbols (
lookup,getDeclaredType) - Type checks (
isInt,isBool, etc.) - Method validation (
methodExists,isValidOverride) - Inheritance support (
isSubtype) - Offset tracking for code generation (
PrintOffset)
Stores metadata for symbols (variables or methods) within a scope.
- Differentiates methods from variables
- Holds:
type: data type or "method"retType: method return typeparamTypes: list of parameter types for methodsoffset: memory layout offset
Represents a symbol table (ST) for a class or method in MiniJava, storing variable and method declarations with type and offset info.
- Stores:
- Variables with type and offset
- Methods with return type, parameter types, and offset
- Supports hierarchical scopes via parent linkage
- Tracks offsets separately for variables and methods
- Can print memory layout (offsets) per class
insert(name, type)– Add a variableinsertMethod(name, retType, paramTypes)– Add a methodlookup(name)– Recursive symbol lookupPrintOffsets(className)– Displays memory layout for variables/methodsaddPointer(),addBool(),addInt(),addMethod()– Offset updates by type
Used in semantic analysis to:
- Track variable/method declarations
- Resolve names
- Manage class/method inheritance and offsets
Builds the symbol table and performs some semantic checks.
- Handles
MainClass,ClassDeclaration, andClassExtendsDeclaration - Supports method declarations, parameters, local variables
- Adds
thisin class/method scopes - Validates method overrides
- Prevents duplicate declarations in scope
- Uses
Scopesto manage scope stack - Method entries stored as
Class_Function_Name - Parameters parsed into type lists for comparison
- Method override rules: same name, return type, and parameter types
Scopes,ST,Info: symbol table structuresyntaxtree.*: JTB-generated AST classes
- This visitor is mainly used to populate the symbol table. It only checks for duplicate declarations and ensures that method overrides are valid.
Performs type checking using the Symbol table provided by the last visitor.
- Verifies return types in methods
- Checks boolean conditions in
if/while - Validates types in:
- Print statements (
int) - Assignments (including subtyping)
- Array operations (indexing, allocation, lookup, length)
- Arithmetic (
+,-,*→int) - Comparisons (
<→boolean) - Logical AND (
&&→boolean) !(logical NOT →boolean)- Method calls (existence, parameter types, return type)
- Print statements (
- Ensures class allocation targets valid classes
- Uses
Scopesto enter existing scopes (enterScopeByName) - Resolves identifiers via
Table.lookup(...) - Replaces variable names with their declared types before comparisons
- Throws exceptions on mismatches
Runs semantic analysis on one or more MiniJava source files by calling both visitors
- Success or failure per file
- Prints semantic errors if any
- Prints offset info
bash
java SemanticAnalyzer MiniJavaSourceFile1 MiniJavaSourceFile2 ...
I have included bass script which runs all tests provided and returns if it succeded or failed the test.
- Purpose: Runs
SemanticAnalyzeron.javafiles inTestFiles - Checks:
- If matching
.txtfile exists inTestFiles/offset-examples, expects success (exit code 0) - Otherwise, expects failure (non-zero exit)
- If matching
- Output: Shows pass/fail per test and summary count
Run:
bash
./test_script.sh
-
Compile:
makeormake compile
Runs JTB, JavaCC, and compiles Java files. -
Clean:
make clean
Removes generated files and.classfiles.
In this part I implemented a simple calculator using a recursive descent parser. The calculator can evaluate arithmetic expressions that include addition, subtraction and exponentiation.
It reads from standard input and evaluates the expression according to the rules defined in the grammar. The parser is implemented in Java, and the program is capable of handling errors gracefully with custom exception handling.
- Basic Arithmetic: Supports
+,-, and**operators. - Parentheses: Allows grouping of expressions using parentheses for correct order of operations.
- Error Handling: If the expression is malformed, a
ParseErroris thrown. - Recursive Descent Parsing: Implements a recursive descent parser to handle the input expression.
expr -> power exprTail exprTail -> + power exprTail | - power exprTail | ε power -> number powerTail powerTail -> ** number powerTail | ε number -> digit numberTail | ( expr ) numberTail -> digit numberTail | ε digit -> 0 - 9
-
Main Function:
The program begins by calling theeval()method, which starts the evaluation process by parsing the expression using theexpr()method. -
Tokenization:
The program processes characters from the input stream using a lookahead approach, consuming tokens one by one. -
Recursive Parsing:
The parser recursively calls various methods to break down and evaluate the expression according to the defined grammar. -
Error Handling:
If the expression does not follow the grammar's rules, aParseErroris thrown to notify the user about the malformed expression.
In this part, I implemented a lexer and two parsers for an expression-only language that supports:
- Concatenation (
+) and reverse operations on strings. - Function definitions and calls.
- If-else statements (with the restriction that every
ifmust be followed by anelse).
-
Input Translation: The input file is translated into an Intermediate Representation (IR), which is a simplified subset of the original language. The IR excludes
=(assignment) and suffix operations insideif-elseblocks. -
Two Parsers:
- IR Parser: Parses the simplified subset of the language (IR).
- Java Parser: Translates the IR into executable Java code.
-
Main Function: Coordinates the process by calling the necessary parsers and performing the translation.
- Test Cases: A set of test cases was created to validate the lexer and parsers.
- Shell Script: A script was written to run the test cases automatically.
- MAKEFILE: A
Makefilewas included to streamline the build and execution process.
I started by creating a lexer based on the tutorial from class. I only made one lexer because I felt a second one wasn’t necessary; the second lexer would essentially be the same as the IR lexer but without the EQ and SUFFIX operations.
The key challenge was developing the IR parser, which removes the equal and suffix operations while maintaining the same functionality. Here's how I approached it:
-
Equal Operation: The
equaloperation was implemented using twoif-elseblocks. The idea behind this is that ifexpr1 prefix expr2andexpr2 prefix expr1both evaluate to true, then the two expressions are considered equal. So, I tested this condition with twoifstatements, and if they weren't equal, theelsepart was executed. -
Suffix Operation: For the
e1 suffix e2operation, I transformed it intoreverse e1 prefix reverse e2. At first, this seemed straightforward, but I encountered an issue with operator precedence. Specifically, when we had an expression likee1+e2 suffix e3(which is valid in our language),reversehas higher precedence, meaning it would only apply toe1and not to the entire concatenation expression.To fix this, I realized that reverse had to have lower precedence than concatenation in this specific scenario. Since parentheses aren't used in this language to manage precedence, I implemented a function that takes an entire expression and reverses it. This way, the function applies the reverse operation to the whole expression (e.g.,
e1+e2), ensuring that the comparison works correctly after the full evaluation of the expression and the correct precedence is kept for all other instances.
For the rest I just ensured that no syntax errors would get accepted from the parser and left everything as is from the input language
This approach allowed me to remove the equal and suffix operations from the IR while still preserving the intended functionality of the language, including proper precedence handling.
-
Class and Function Definitions:
The Java parser closely follows the same grammar as the IR parser, but the primary challenge here was translating the IR into valid Java code. I started by adding theclassdeclaration and converting the function definitions into their correct Java equivalents. The conversion process was relatively straightforward, as the language’s structure is similar to Java in terms of function syntax.
The only adjustments needed were:- Adding the return type (
Stringin our case). - Adding the
returnstatement at the end of the function body.
- Adding the return type (
-
If-Else Statements:
In our language, if-else statements are considered expressions, meaning they can appear anywhere — in function calls, inside other if statements, etc. This made translating them a bit tricky.
I observed that because everyifmust be followed by anelseand only one expression can appear inside either theiforelseblock, the functionality of theif-elsestatement is essentially equivalent to the ternary operator in Java. Since the ternary operator is also an expression in Java and can be used anywhere, I decided to translate if-else statements into ternary expressions. -
Prefix Operation:
In our language, prefix isn't a keyword and has no direct equivalent in Java. As a result, I had to translate theprefixoperation into something Java understands.
I used thestartsWith()method, which is a built-in Java function that applies directly to strings and returns a boolean. This boolean value is then used in the ternary expression to determine which part of the code should be executed, based on whether the condition evaluates totrueorfalse. -
Other Operations:
For the rest of the operations, I ensured the correct translation into Java. The syntax of the language was similar enough to Java that not many modifications were needed. Most changes involved adding the appropriate types in front of functions and modifying some operations to their correct Java equivalents.
This process allowed me to successfully translate the language from the IR to Java, ensuring that the program could handle expressions correctly while maintaining functionality and precedence rules. The translation was straightforward for most parts due to the structural similarity between the two languages. With proper handling of edge cases like if-else expressions and prefix operations, I achieved a clean and functional Java implementation.