Project 1: C Declaration Parser (cdecl Clone)
Build a command-line tool that parses complex C declarations and translates them to English using the clockwise/spiral rule.
Quick Reference
| Attribute | Value |
|---|---|
| Language | C |
| Difficulty | Level 3 (Intermediate) |
| Time | Weekend (8-16 hours) |
| Book Reference | Expert C Programming, Chapter 3 |
| Coolness | Interview Gold - Classic systems question |
| Portfolio Value | High - Demonstrates parsing skills |
Learning Objectives
By completing this project, you will:
- Master C declaration syntax - Read any declaration without hesitation, from simple to arcane
- Implement the clockwise/spiral rule - Transform the mental algorithm into working code
- Understand operator precedence in declarations - Know why
int *p[10]differs fromint (*p)[10] - Parse context-free grammar manually - Build a recursive descent parser for C declarations
- Distinguish declarators from specifiers - Separate type information from identifier binding
- Handle type qualifiers correctly - Understand where
constbinds in different positions - Build a lexer-parser system - Create tokens from text and build structure from tokens
- Generate human-readable output - Translate abstract syntax into clear English
The Core Question You’re Answering
“Why does C declaration syntax read inside-out and right-to-left, and how can we systematically decode any declaration?”
C declarations are notoriously confusing because they were designed to mimic usage. The declaration int *p says that *p has type int, meaning p is a pointer to int. This “declaration follows use” principle, combined with operator precedence, creates the seemingly bizarre syntax where you must read declarations in a spiral pattern from the identifier outward.
Understanding this deeply means you can:
- Read any declaration in legacy codebases
- Debug function pointer callback issues
- Write complex type definitions correctly
- Ace systems programming interviews
Theoretical Foundation
Why C Declarations Are Hard
Dennis Ritchie designed C declarations so that the declaration of a variable mimics its use. This principle, while elegant in theory, leads to confusing syntax:
Declaration Follows Use:
int x; // x is an int
int *p; // *p is an int, so p is a pointer to int
int a[10]; // a[i] is an int, so a is an array of int
int f(); // f() is an int, so f is a function returning int
The problem comes when these combine:
Complex Declaration Analysis:
char *(*fp)(int, float);
Reading this requires understanding:
1. Identifier is 'fp'
2. ( ) groups 'fp' with the first '*'
3. So fp is a pointer to... something
4. (int, float) means that something is a function
5. Returning char * (pointer to char)
Result: "fp is a pointer to a function taking (int, float) returning pointer to char"
The Clockwise/Spiral Rule
The clockwise/spiral rule, documented in Expert C Programming Chapter 3, provides a systematic way to read any C declaration:
THE CLOCKWISE/SPIRAL RULE ALGORITHM:
=====================================
Step 1: Find the identifier (the variable/function name)
Step 2: Start reading "spiral clockwise":
- Go right: read array bounds [ ] or function parameters ( )
- Go left: read pointer symbols * and type qualifiers
- Repeat until you've consumed all elements
Step 3: Parentheses ( ) act as grouping - they redirect the spiral
VISUAL EXAMPLE: char *(*fp)(int, float)
=====================================================
┌─────────────────────────────┐
│ ┌──────────────┐ │
│ │ ┌─────┐ │ │
│ │ │ │ │ │
▼ ▼ ▼ │ │ │
char * ( * fp ) (int, float)
▲ ▲ │ ▲
│ │ │ │
│ │ └───┘
│ └──────────────────────
└───────────────────────────────
Reading order:
1. Start at 'fp'
2. Go right: hit ')', so bounce back
3. Go left: '*' → "pointer to"
4. Go right past ')': '(int, float)' → "function taking int and float"
5. Go left: '*' → "returning pointer to"
6. Go left: 'char' → "char"
Result: "fp is a pointer to a function(int, float) returning pointer to char"
Declaration Syntax Components
A C declaration has two main parts:
DECLARATION STRUCTURE:
======================
┌────────────────────────────────────────────────────┐
│ static const unsigned long int │ Declaration Specifiers
│ ───┬── ──┬── ───┬──── ──┬─ ─┬─ │ (storage class, qualifiers, type)
│ │ │ │ │ │ │
│ storage type sign size type │
│ class qualifier modifier modifier │
└────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────┐
│ *(*fp)[10] │ Declarator
│ ─┬───┬───┬─ │ (how to derive the type)
│ │ │ │ │
│ pointer identifier array │
│ modifier modifier │
└────────────────────────────────────────────────────┘
Operator Precedence in Declarations
Understanding precedence is critical:
DECLARATION PRECEDENCE (highest to lowest):
===========================================
1. ( ) - Function call or grouping parentheses
2. [ ] - Array subscript
3. * - Pointer dereference
EXAMPLES:
=========
int *p[10]; // Array of 10 pointers to int
// [] binds tighter than *
// Read: "p is array[10] of pointer to int"
int (*p)[10]; // Pointer to array of 10 ints
// () groups * with p first
// Read: "p is pointer to array[10] of int"
int *f(); // Function returning pointer to int
// () binds tighter than *
// Read: "f is function returning pointer to int"
int (*f)(); // Pointer to function returning int
// () groups * with f first
// Read: "f is pointer to function returning int"
Type Qualifiers and const Placement
The const keyword binds to what’s immediately to its left (or right if nothing is to its left):
CONST BINDING RULES:
====================
const int *p; // Pointer to const int
// *p cannot be modified, p can
int const *p; // Same as above (const after type)
// *p cannot be modified, p can
int * const p; // Const pointer to int
// p cannot be modified, *p can
const int * const p; // Const pointer to const int
// Neither p nor *p can be modified
VISUALIZATION:
==============
const int * p;
──────┬── ▲
│ │
modifies what p points to (the int)
int * const p;
▲ ──────┬
│ │
│ modifies p itself (the pointer)
│
the pointed-to int is not const
The Grammar (Simplified)
A simplified grammar for C declarations:
declaration → specifiers declarator
specifiers → (storage-class | type-qualifier | type-specifier)+
declarator → pointer? direct-declarator
pointer → '*' type-qualifier* pointer?
direct-declarator → identifier
| '(' declarator ')'
| direct-declarator '[' constant? ']'
| direct-declarator '(' parameter-list? ')'
Project Specification
What You Will Build
A command-line tool called cdecl that:
- Accepts a C declaration as input
- Parses it using the clockwise/spiral algorithm
- Outputs an English translation
Functional Requirements
- Basic Type Parsing:
- Handle basic types:
int,char,float,double,void - Handle signed/unsigned modifiers
- Handle short/long modifiers
- Handle basic types:
- Pointer Support:
- Single and multiple levels of indirection (
*,**,***) - Const and volatile qualifiers on pointers
- Single and multiple levels of indirection (
- Array Support:
- Fixed-size arrays
[10] - Unsized arrays
[] - Multi-dimensional arrays
[10][20]
- Fixed-size arrays
- Function Support:
- Function declarations with parameter lists
- Function pointers
- Functions returning pointers
- Complex Combinations:
- Arrays of pointers
- Pointers to arrays
- Pointers to functions
- Functions returning pointers to arrays
- Error Handling:
- Invalid syntax detection
- Helpful error messages
Non-Functional Requirements
- Performance: Parse declarations in under 10ms
- Portability: Works on Linux, macOS, Windows
- Code Quality: Clean, well-documented code
- Testing: Comprehensive test suite
Real World Outcome
When complete, your tool will handle declarations like the famous signal function:
$ ./cdecl "void (*signal(int sig, void (*func)(int)))(int)"
signal is a function taking (int sig, pointer to function taking (int) returning void) returning pointer to function taking (int) returning void
$ ./cdecl "char *(*fp)(int, float)"
fp is a pointer to a function taking (int, float) returning pointer to char
$ ./cdecl "int (*(*callbacks[10])(int))(void)"
callbacks is an array[10] of pointer to function taking (int) returning pointer to function taking (void) returning int
$ ./cdecl "const char * const *pp"
pp is a pointer to const pointer to const char
$ ./cdecl "char *(*(*x)(void))[5]"
x is a pointer to function taking (void) returning pointer to array[5] of pointer to char
Solution Architecture
High-Level Design
┌──────────────────────────────────────────────────────────────┐
│ CDECL TOOL │
├──────────────────────────────────────────────────────────────┤
│ │
│ "char *(*fp)(int)" │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ LEXER │──▶ [CHAR] [*] [(] [*] [ID:fp] [)] [(] ... │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌──────────────────────────────┐ │
│ │ PARSER │────▶│ AST: │ │
│ │ (Spiral) │ │ POINTER TO │ │
│ └─────────────┘ │ FUNCTION (int) │ │
│ │ │ RETURNING │ │
│ │ │ POINTER TO │ │
│ ▼ │ CHAR │ │
│ ┌─────────────┐ └──────────────────────────────┘ │
│ │ GENERATOR │ │
│ │ (English) │ │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ "fp is a pointer to function(int) returning pointer to char"│
│ │
└──────────────────────────────────────────────────────────────┘
Data Structures
/* Token types for the lexer */
typedef enum {
TOK_IDENTIFIER,
TOK_TYPE, /* int, char, void, etc. */
TOK_QUALIFIER, /* const, volatile */
TOK_STORAGE, /* static, extern, register */
TOK_POINTER, /* * */
TOK_LPAREN, /* ( */
TOK_RPAREN, /* ) */
TOK_LBRACKET, /* [ */
TOK_RBRACKET, /* ] */
TOK_COMMA, /* , */
TOK_NUMBER, /* array sizes */
TOK_EOF,
TOK_ERROR
} TokenType;
typedef struct {
TokenType type;
char value[64];
int position;
} Token;
/* Declaration component types */
typedef enum {
COMP_POINTER, /* * */
COMP_ARRAY, /* [N] */
COMP_FUNCTION, /* (params) */
COMP_CONST, /* const */
COMP_VOLATILE, /* volatile */
} ComponentType;
typedef struct Component {
ComponentType type;
char data[256]; /* e.g., array size, parameter list */
struct Component *next;
} Component;
/* Parsed declaration */
typedef struct {
char identifier[64];
char base_type[128]; /* e.g., "unsigned long int" */
Component *components; /* Stack of modifiers */
} Declaration;
Key Algorithms
Tokenization:
Algorithm: Tokenize(input)
1. Skip whitespace
2. If letter: read identifier/keyword, classify as TYPE/QUALIFIER/IDENTIFIER
3. If digit: read number (for array sizes)
4. If '*': emit POINTER token
5. If '(', ')', '[', ']', ',': emit corresponding token
6. Repeat until end of input
Spiral Rule Parsing:
Algorithm: ParseDeclarator(tokens)
1. Find the identifier (rightmost identifier in current context)
2. Read RIGHT from identifier:
- If '(' for function: parse parameter list, record FUNCTION component
- If '[' for array: parse size, record ARRAY component
- If ')': stop going right (hit grouping paren)
3. Read LEFT from identifier:
- If '*': record POINTER component
- If 'const'/'volatile': attach to nearest pointer
- If '(': this was grouping, expand scope and repeat from step 2
4. Continue until base type reached
5. Record base type (int, char, etc.)
English Generation:
Algorithm: GenerateEnglish(declaration)
1. Output: "{identifier} is "
2. For each component (in reverse order):
- POINTER → "pointer to "
- ARRAY[N] → "array[N] of "
- FUNCTION(params) → "function taking (params) returning "
- CONST → "const "
3. Output: base_type
Implementation Guide
Phase 1: Lexer (Day 1 Morning)
Goal: Convert input string to token stream.
/* lexer.h */
#ifndef LEXER_H
#define LEXER_H
#define MAX_TOKENS 100
#define MAX_TOKEN_LEN 64
typedef enum { /* ... as defined above ... */ } TokenType;
typedef struct { /* ... as defined above ... */ } Token;
typedef struct {
Token tokens[MAX_TOKENS];
int count;
int current;
} TokenStream;
int tokenize(const char *input, TokenStream *stream);
Token peek(TokenStream *stream);
Token advance(TokenStream *stream);
int match(TokenStream *stream, TokenType expected);
#endif
Implementation hints:
- Use
isalpha()andisdigit()for character classification - Keep a table of keywords (
int,char,const, etc.) - Store position for error messages
- Handle both
const intandint const
Phase 2: Parser Core (Day 1 Afternoon)
Goal: Implement the spiral rule parser.
Key insight: Use a stack to track modifiers as you spiral outward:
/* parser.h */
typedef struct {
Component *stack; /* Stack of modifiers found */
char identifier[64];
char base_type[128];
int error;
char error_msg[256];
} ParseResult;
ParseResult parse_declaration(TokenStream *tokens);
Parsing strategy:
- First pass: find the identifier
- Second pass: spiral outward collecting components
- Handle grouping parentheses by recursion
Phase 3: Complex Declarations (Day 2 Morning)
Goal: Handle nested declarations like function pointers.
The key challenge is handling declarations like:
void (*signal(int, void (*)(int)))(int)
Strategy:
- When you hit
(after*, it could be:- Grouping:
(*fp)- the*applies tofp - Function:
*func()- function returning pointer
- Grouping:
- Distinguish by looking ahead: if identifier follows
*, it’s grouping
Phase 4: English Generator (Day 2 Afternoon)
Goal: Convert parsed structure to readable English.
void generate_english(Declaration *decl, char *output, size_t size);
Walk the component stack and generate appropriate English text:
- Keep track of whether you’ve started output
- Handle singular/plural (“pointer” vs “pointers”)
- Format parameter lists cleanly
Hints in Layers
Hint 1: Getting Started
Start with the simplest cases and verify with cdecl.org:
// Level 1: Single modifiers
"int x" → "x is int"
"int *p" → "p is pointer to int"
"int a[10]" → "a is array[10] of int"
"int f()" → "f is function returning int"
// Level 2: Two modifiers
"int *a[10]" → "a is array[10] of pointer to int"
"int (*p)[10]" → "p is pointer to array[10] of int"
Get these working before moving to complex cases.
Hint 2: Finding the Identifier
The identifier is always at the “core” of the declaration. Strategy:
- Skip any leading type specifiers
- Follow
*symbols and(parentheses - The first identifier you hit is the one you want
// Identifier finding examples:
"int x" → x is after "int "
"int *p" → p is after "* "
"int (*fp)()" → fp is inside parentheses
"int *(*fp)()" → fp is in the deepest parentheses
Hint 3: Handling Precedence
The spiral works because of precedence. () and [] bind tighter than *.
// This is why:
int *a[10]; // a binds to [10] first, then to *
// = array of pointers
int (*a)[10]; // ( ) groups * with a first
// = pointer to array
When implementing, process right-side modifiers ((), []) before left-side (*).
Hint 4: Recursive Structure
Declarations are naturally recursive. When you see (*...), treat the contents as a sub-declaration:
int (*fp)();
^^^
This is a declarator: "*fp"
It means "fp is a pointer"
int (*(*fp)())();
^^^^^
Inner declarator: "*fp"
Then we wrap: "pointer to function returning..."
Use recursive descent: parse_declarator() calls itself when it encounters grouping parens.
Hint 5: Data Structure for Components
Use a linked list or array to store components in order:
// For: char *(*fp)(int, float)
// Components (inside out):
// 1. POINTER (from first *)
// 2. FUNCTION with params "int, float"
// 3. POINTER (from second *)
// 4. Base type: char
struct Component components[] = {
{ COMP_POINTER, "" },
{ COMP_FUNCTION, "int, float" },
{ COMP_POINTER, "" },
};
char base_type[] = "char";
Hint 6: English Generation Template
Generate English by walking components in reverse:
// Template:
// "{identifier} is {component_n} ... {component_1} {base_type}"
// For components:
// POINTER → "pointer to "
// ARRAY → "array[SIZE] of "
// FUNCTION → "function taking (PARAMS) returning "
// CONST → "const "
// VOLATILE → "volatile "
Testing Strategy
Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | Test lexer and parser components | Tokenization correctness |
| Integration Tests | Test full pipeline | Complete declarations |
| Regression Tests | Ensure fixes don’t break | Known tricky cases |
| Fuzz Tests | Find edge cases | Random valid inputs |
Critical Test Cases
// Basic types
TEST("int x", "x is int")
TEST("char c", "c is char")
TEST("void v", "v is void")
TEST("float f", "f is float")
TEST("double d", "d is double")
// Pointers
TEST("int *p", "p is pointer to int")
TEST("int **pp", "pp is pointer to pointer to int")
TEST("int ***ppp", "ppp is pointer to pointer to pointer to int")
// Arrays
TEST("int a[10]", "a is array[10] of int")
TEST("int a[]", "a is array of int")
TEST("int a[10][20]", "a is array[10] of array[20] of int")
// Functions
TEST("int f()", "f is function returning int")
TEST("int f(int)", "f is function taking (int) returning int")
TEST("int f(int, char)", "f is function taking (int, char) returning int")
// Pointers and arrays
TEST("int *a[10]", "a is array[10] of pointer to int")
TEST("int (*a)[10]", "a is pointer to array[10] of int")
// Function pointers
TEST("int (*fp)()", "fp is pointer to function returning int")
TEST("int (*fp)(int, int)", "fp is pointer to function taking (int, int) returning int")
// Returning pointers
TEST("int *f()", "f is function returning pointer to int")
TEST("int **f()", "f is function returning pointer to pointer to int")
// Complex: function pointer returning pointer
TEST("char *(*fp)(int, float)",
"fp is pointer to function taking (int, float) returning pointer to char")
// The signal declaration
TEST("void (*signal(int, void (*)(int)))(int)",
"signal is function taking (int, pointer to function taking (int) returning void) returning pointer to function taking (int) returning void")
// Const variations
TEST("const int *p", "p is pointer to const int")
TEST("int const *p", "p is pointer to const int")
TEST("int * const p", "p is const pointer to int")
TEST("const int * const p", "p is const pointer to const int")
Testing Script
#!/bin/bash
# test_cdecl.sh
run_test() {
input="$1"
expected="$2"
result=$(./cdecl "$input")
if [ "$result" = "$expected" ]; then
echo "PASS: $input"
else
echo "FAIL: $input"
echo " Expected: $expected"
echo " Got: $result"
fi
}
run_test "int x" "x is int"
run_test "int *p" "p is pointer to int"
run_test "int (*fp)()" "fp is pointer to function returning int"
# ... more tests ...
Common Pitfalls & Debugging
Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Identifier confusion | Wrong name extracted | Look for rightmost identifier in deepest parens |
| Precedence errors | int *a[10] parsed wrong |
Remember: [] binds tighter than * |
| Const placement | const int* vs int* const |
Const binds left, or right if leftmost |
| Missing recursion | Nested parens fail | Call parser recursively on grouped content |
| Token lookahead | Can’t distinguish cases | Implement peek() to look ahead without consuming |
| Parameter parsing | Function params wrong | Parse params as comma-separated list |
Debugging Strategies
- Print token stream: Verify lexer output
void debug_tokens(TokenStream *s) { for (int i = 0; i < s->count; i++) { printf("[%d] %s: '%s'\n", i, token_type_name(s->tokens[i].type), s->tokens[i].value); } } - Print parse tree: Visualize component structure
void debug_components(Component *c, int depth) { while (c) { printf("%*s%s: %s\n", depth*2, "", component_type_name(c->type), c->data); c = c->next; } } - Compare with cdecl.org: Verify expected output
- Test incrementally: Add one feature at a time
Tricky Cases
// These look similar but are different:
int (*p)[10] // p is pointer to array[10] of int
int *p[10] // p is array[10] of pointer to int
// These need careful const handling:
const char *s // pointer to const char (string literal safe)
char const *s // same as above
char *const s // const pointer to char (can modify string, not pointer)
const char *const s // const pointer to const char
// Nested function pointers:
int (*(*fp)(int))(double)
// fp is pointer to function(int) returning pointer to function(double) returning int
Extensions & Challenges
Beginner Extensions
- Interactive mode: Read declarations from stdin interactively
- Reverse mode: Given English, generate C declaration
- Typedef handling: Parse and expand typedef’d types
- Colorized output: Highlight different parts of the declaration
Intermediate Extensions
- Full C grammar: Handle struct, union, enum declarations
- Variadic functions: Handle
...in parameter lists - Storage classes: Parse
static,extern,register,auto - Declaration file: Parse entire .h files
Advanced Extensions
- Modern C: Support C11/C17 features (
_Atomic,_Generic) - C++ support: Handle references, templates (limited)
- AST output: Generate machine-readable parse tree (JSON)
- Error recovery: Continue parsing after errors
Real-World Connections
Industry Applications
- Header analysis tools: Understanding API declarations
- Documentation generators: Doxygen-style tools
- IDE features: Declaration tooltips and navigation
- Refactoring tools: Type-aware code transformation
- Compiler frontends: First step in compilation
Related Open Source Projects
- cdecl: The original C declaration explainer (1980s)
- cgreen/cppcheck: C code analysis tools
- clang-tidy: Uses AST analysis for code checking
- LLVM/Clang: Full C parser implementation
Interview Relevance
This is a classic systems programming interview topic:
- “Explain what
void (*signal(int, void (*)(int)))(int)means”- The
signalfunction declaration from<signal.h> - Tests deep understanding of C syntax
- The
- “What’s the difference between
const char *andchar const *?”- Tests understanding of const placement
- Often followed by
char * const
- “Write a function that takes a function pointer and returns a function pointer”
- Tests practical declaration skills
- Common in callback-heavy systems code
- “Parse a C declaration by hand on the whiteboard”
- Demonstrate the spiral rule
- Walk through step by step
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Declaration syntax | Expert C Programming | Ch. 3 “Unscrambling Declarations in C” |
| C grammar | The C Programming Language (K&R) | Appendix A |
| Parsing techniques | Compilers (Dragon Book) | Ch. 4 “Syntax Analysis” |
| Type system | C: A Reference Manual | Ch. 4 “Declarations” |
| Modern C | Effective C, 2nd Ed | Ch. 3 “Types” |
Self-Assessment Checklist
Understanding
- I can explain why C declarations read “inside-out”
- I can apply the clockwise/spiral rule to any declaration
- I understand why
int *a[10]differs fromint (*a)[10] - I can explain where
constbinds in different positions - I understand the difference between specifiers and declarators
Implementation
- My lexer correctly tokenizes all C declaration elements
- My parser handles basic pointers, arrays, and functions
- My parser handles nested function pointers
- My parser handles const and volatile qualifiers
- My English generator produces correct, readable output
Testing
- All basic test cases pass
- Complex cases like
signalwork correctly - Error handling provides helpful messages
- I’ve verified against cdecl.org
Growth
- I can read C declarations in real codebases confidently
- I can write complex declarations correctly on the first try
- I understand how this relates to typedef usage
Submission / Completion Criteria
Minimum Viable Completion
- Handles basic types (int, char, void, float, double)
- Parses single-level pointers and arrays
- Parses simple function declarations
- Produces readable English output
Full Completion
- Handles all basic types with modifiers (unsigned, long, short)
- Parses multi-level pointers and multi-dimensional arrays
- Parses function pointers and functions returning pointers
- Handles const and volatile qualifiers correctly
- Comprehensive test suite passing
Excellence (Going Above & Beyond)
- Handles the signal() declaration correctly
- Interactive mode with readline support
- Reverse mode (English to C)
- Full C11 type support
- Error recovery and helpful diagnostics
Thinking Exercise
Before writing code, trace through these declarations by hand. Write out each step of the spiral rule:
Exercise 1: char *argv[]
Step 1: Find identifier →
Step 2: Go right →
Step 3: Go left →
Step 4: Go left →
Result:
Exercise 2: int (*(*callbacks[10])(int))(void)
Step 1: Find identifier →
Step 2: Go right →
Step 3: Go left →
Step 4: Continue spiral... →
Result:
Exercise 3: const char * const * const pp
Step 1: Find identifier →
Step 2: Trace const binding →
Result:
Interview Questions They’ll Ask
After completing this project, you’ll be ready for:
- “What is the clockwise/spiral rule?”
- Explain the algorithm step by step
- Demonstrate with an example
- “Parse
void (*f(int, void (*)(int)))(int)and explain it”- This is the signal() signature
- Walk through the spiral systematically
- “What’s the difference between
int *const pandconst int *p?”- Explain const binding rules
- Give practical examples of when each is used
- “Why does C use this declaration syntax?”
- “Declaration follows use” principle
- Historical context from B and BCPL
- “How would you implement a parser for C declarations?”
- Discuss tokenization approach
- Explain recursive descent for nested structures
- Mention precedence handling
This guide was expanded from EXPERT_C_PROGRAMMING_DEEP_DIVE.md. For the complete learning path, see the project index.