CD Notes
Build an advanced mathematical expression parser with tree visualization
Introduction and Goals
An expression parser takes mathematical expressions like 2 + 3 * 4 or sin(pi/2) + sqrt(16) and understands their structure — respecting operator precedence, associativity, and function application. This project goes beyond a simple calculator by building and visualizing the parse tree, demonstrating how the hierarchical structure of an expression determines its meaning.
This is an ideal second project after the basic calculator because it introduces several new concepts: right-associative operators (exponentiation), unary operators (negation), function calls as expressions, variables, and tree visualization. By the end, you will have a tool that can display the parse tree for any mathematical expression, making abstract parsing concepts concrete and visual.
Language Specification
Our expression parser handles a rich set of mathematical constructs:
| - Integer and floating-point literals | 42, 3.14, .5, 1e10 |
| - Arithmetic operators | +, -, *, /, % (modulo) |
| - Exponentiation | ^ (right-associative: 2^3^2 = 2^9 = 512) |
| - Unary negation | -5, -(3+4) |
| - Parentheses | (2+3) * 4 |
| - Functions | sin, cos, tan, sqrt, abs, log, exp |
| - Constants | pi, e |
| - Variables | x, y, z (with assignment) |
Operator Precedence Table
Precedence determines evaluation order. Higher precedence binds tighter:
| Level 1 (lowest) | +, - Binary addition/subtraction (left-associative) |
| Level 2 | *, /, % Multiplication/division (left-associative) |
| Level 3 | ^ Exponentiation (RIGHT-associative) |
| Level 4 | - (unary) Negation (prefix, right-to-left) |
| Level 5 (highest) | func() Function application (prefix) |
The grammar naturally encodes this hierarchy — each precedence level gets its own grammar rule:
| expression | term (('+' | '-') term)* |
| term | power (('*' | '/' | '%') power)* |
| power | unary ('^' power)? // Right-recursive for right-assoc |
| unary | '-' unary | postfix |
| postfix | primary | FUNC_NAME '(' expression ')' |
| primary | NUMBER | VARIABLE | '(' expression ')' |
Notice the critical difference: power uses '^' power (recursive call to itself) rather than '^' unary. This makes exponentiation right-associative. For 2^3^2, the parser first processes 3^2 = 9, then 2^9 = 512.
Implementation: Recursive Descent Parser
Each grammar rule becomes a C function. The code directly mirrors the grammar structure:
Building a Parse Tree (AST Version)
Instead of evaluating directly, build a tree structure for visualization:
Tracing Through Examples
Example 1: 2 + 3 * 4
| parse_power | parse_unary → parse_primary → returns 2 |
| No * or / | term returns 2 |
| Token is '+' | advance, call parse_term again |
| parse_power | parse_primary → returns 3 |
| Token is '*' | advance, call parse_power |
| parse_primary | returns 4 |
Example 2: 2^3^2 (right-associative)
| parse_unary | returns 2 |
| Token is '^' | advance, call parse_power RECURSIVELY |
| parse_unary | returns 3 |
| Token is '^' | advance, call parse_power RECURSIVELY |
| parse_unary | returns 2 |
| No '^' | returns 2 |
Example 3: sin(pi/2) + 1
| parse_term | parse_power → parse_unary → parse_postfix |
| Token is FUNC "sin" | advance, expect '(' |
| Token is '/' | advance |
| No * | term returns 1.0 |
| Token is '+' | advance |
| parse_term | parse_primary → returns 1 |
Tree Visualization
Print the parse tree in a readable format:
Output for (a + b) * (c - d):
Variable Support
Add a simple environment for variable storage:
Error Handling
Provide clear, specific error messages:
void error(Parser *p, const char *expected) {
fprintf(stderr, "Error at position %d: %s\n", p->position, expected);
fprintf(stderr, " Input: %s\n", p->input);
fprintf(stderr, " %*s^\n", p->position + 9, ""); // Point to error
}Key Takeaways
This project demonstrates that operator precedence and associativity are naturally encoded in the grammar hierarchy. Each precedence level corresponds to a grammar rule and a parser function. Right-associativity is achieved through right-recursion (the function calls itself for the right operand), while left-associativity uses iteration (a while loop consuming operators of equal precedence). Building and visualizing parse trees makes these abstract concepts tangible, showing exactly how the parser's structure determines the mathematical meaning of every expression.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Expression Parser Project.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Compiler Design topic.
Search Terms
compiler-design, compiler design, compiler, design, projects, expression, parser, project
Related Compiler Design Topics