SysTutorials

ANSI C Grammar Definition in YACC

ANSI C is the standardized version of C defined in 1989, specifying the language’s syntax and semantics formally through a grammar. Understanding this grammar is essential for building tools that parse, analyze, or transform C code.

Grammar Basics

The ANSI C grammar is defined using Backus-Naur Form (BNF), a notation for specifying formal grammars. The grammar consists of production rules that define how non-terminal symbols (syntactic categories like expressions, statements, declarations) derive from other non-terminals and terminal symbols (keywords, operators, identifiers, literals).

A simple production rule looks like:

declaration:
    declaration_specifiers init_declarator_list_opt ';'

This defines a declaration as type specifiers followed by an optional declarator list, terminated by a semicolon. The _opt suffix indicates an optional element. Complex grammars contain hundreds of such rules covering expressions, statements, function definitions, type qualifiers, and more.

Working with Yacc (Bison)

Yacc (Yet Another Compiler Compiler) and its modern replacement Bison are parser generators that take a formal grammar specification and generate a C parser. This approach lets you define language syntax declaratively rather than hand-coding parsing logic.

A minimal Yacc grammar file for C declarations might look like:

%{
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void yyerror(const char *s) {
    fprintf(stderr, "Parse error: %s\n", s);
}
%}

%token INT FLOAT CHAR IDENTIFIER SEMICOLON
%token ASSIGN NUMBER

%%

program:
    declarations
    ;

declarations:
    declaration
    | declarations declaration
    ;

declaration:
    type IDENTIFIER SEMICOLON
    | type IDENTIFIER ASSIGN NUMBER SEMICOLON
    ;

type:
    INT | FLOAT | CHAR
    ;

%%

int main() {
    return yyparse();
}

To compile this:

bison -d grammar.y
gcc -o parser grammar.tab.c lex.yy.c -lm

You’ll also need a corresponding Lex (or Flex) lexer file to tokenize input:

%{
#include "grammar.tab.h"
%}

%%
"int"       { return INT; }
"float"     { return FLOAT; }
"char"      { return CHAR; }
[a-zA-Z_][a-zA-Z0-9_]* { return IDENTIFIER; }
[0-9]+      { return NUMBER; }
"="         { return ASSIGN; }
";"         { return SEMICOLON; }
[ \t\n]    { /* skip whitespace */ }
.           { fprintf(stderr, "Unexpected character: %c\n", yytext[0]); }
%%

int yywrap() { return 1; }

Compile together:

flex grammar.l
bison -d grammar.y
gcc -o parser grammar.tab.c lex.yy.c -lm
echo "int x;" | ./parser

Complete ANSI C Grammar References

For production work, reference implementations are available:

The grammar specification covers:

Practical Applications

Parsing ANSI C is useful for:

For most production scenarios, using existing libraries like libclang or pycparser is more practical than writing your own parser. However, understanding the grammar itself is crucial for debugging parser issues, extending the language, or building specialized analysis tools.

Modern projects like tree-sitter also provide prebuilt grammars for C with incremental parsing capabilities, making them suitable for IDE integration and real-time analysis.

← All posts