Mastering ANTLR4: From Grammar Definition to Tree Processing

ANTLR4 is a powerful parser generator toolkit that produces clean, maintainable lexer and parser code for multiple target languages. Its intuitive grammar syntax, built-in support for operator precedence, and flexible tree-walking APIs make it ideal for building domain-specific languages, configuration parsers, and language tooling.

Setup and Toolchain Configuration

To begin using ANTLR4 from the command line, download the runtime JAR and configure shell aliases:

Add to ~/.zshrc or ~/.bashrc

export CLASSPATH=".:/usr/local/lib/antlr/antlr-4.13.1-complete.jar:$CLASSPATH" alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr/antlr-4.13.1-complete.jar:$CLASSPATH" org.antlr.v4.Tool' alias grun='java -cp "/usr/local/lib/antlr/antlr-4.13.1-complete.jar:$CLASSPATH" org.antlr.v4.gui.TestRig'


</div>Grammar Structure Fundamentals
------------------------------

ANTLR4 grammars follow strict naming and casing conventions:

- Grammar name must match the filename (e.g., `Expr.g4` declares `grammar Expr;`)
- Parser rules start with lowercase letters
- Lexer rules (tokens) start with uppercase letters
- All lexer rules are automatically grouped after parser rules during compilation
- Rules are matched in declaration order—earlier definitions take precedence
- Literals use single quotes: `'if'`, `'=='`
- Alternatives use `|`; grouping uses parentheses; quantifiers `?`, `+`, `*` behave like regex
- Rule alternatives can be labeled with `# label` to generate distinct context classes
- No explicit start rule required—the first parser rule serves as default entry point
- Semicolons terminate all rules
- Comments: `// line` and `/* block */`
- Associativity defaults to left; override with `<assoc=right>`
- Direct left recursion is supported; indirect left recursion is not
- Use `fragment` to define reusable lexer subrules

### Example Grammar Snippet

<div class="code-block">```
program: statement+ ;

statement
  : expression NEWLINE                 # showResult
  | IDENTIFIER '=' expression NEWLINE  # storeValue
  | NEWLINE                            # skipEmpty
  ;

expression
  : expression '^' expression          # powerExpr
  | expression ('*'|'/') expression    # productExpr
  | expression ('+'|'-') expression    # sumExpr
  | NUMBER                             # numberLiteral
  | IDENTIFIER                         # variableRef
  | '(' expression ')'                 # groupedExpr
  ;

IDENTIFIER : [a-zA-Z_][a-zA-Z_0-9]* ;
NUMBER     : [0-9]+ ('.' [0-9]+)? | '.' [0-9]+ ;
NEWLINE    : '\r'? '\n' ;
WS         : [ \t\r\n]+ -> skip ;

Reusable lexer constructs simplify grammar maintenance:

// Keywords (case-sensitive) IF : 'if' ; ELSE : 'else' ; WHILE : 'while' ;

// Identifiers IDENTIFIER : LETTER (LETTER | DIGIT)* ; fragment LETTER : [a-zA-Z_] ; fragment DIGIT : [0-9] ;

// Numeric literals INTEGER : DIGIT+ ; FLOAT : DIGIT+ '.' DIGIT* | '.' DIGIT+ ; SCIENTIFIC : (INTEGER | FLOAT) [eE] [+-]? DIGIT+ ;

// Strings with escape handling STRING : '"' (~["\\r\n] | ESCAPE)* '"' ; fragment ESCAPE : '\' ([btnrf"'\] | UNICODE) ; fragment UNICODE : 'u' HEX HEX HEX HEX ; fragment HEX : [0-9a-fA-F] ;

// Comments LINE_COMMENT : '//' ~[\r\n]* -> skip ; BLOCK_COMMENT : '/' .? '*/' -> skip ;


</div>Code Generation Options
-----------------------

Control generated artifacts using CLI flags:

<div class="code-block">```
$ antlr4 -visitor -no-listener Expr.g4
$ ls
Expr.g4             ExprBaseVisitor.java  ExprListener.java
Expr.tokens         ExprLexer.java        ExprParser.java
ExprBaseListener.java ExprLexer.tokens    ExprVisitor.java

  • ExprLexer.java: Tokenizes input character streams
  • ExprParser.java: Builds parse trees from token streams
  • ExprVisitor.java & ExprBaseVisitor.java: Visitor pattern interfaces and stubs
  • ExprListener.java & ExprBaseListener.java: Listener pattern interfaces and stubs
  • Expr.tokens: Token type constants mapping

Integrating Generated Code

A minimal driver program demonstrates the full pipeline:

public class ExpressionEvaluator { public static void main(String[] args) throws Exception { CharStream input = CharStreams.fromFileName("input.txt"); ExprLexer lexer = new ExprLexer(input); CommonTokenStream tokens = new CommonTokenStream(lexer); ExprParser parser = new ExprParser(tokens);

ParseTree tree = parser.program(); // entry rule

// Visualize structure
System.out.println("Parse tree (LISP format):");
System.out.println(tree.toStringTree(parser));

// Evaluate using visitor
EvalVisitor evaluator = new EvalVisitor();
evaluator.visit(tree);

// Or walk with listener
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(new SymbolCollector(), tree);

} }


</div>Visitor Pattern Deep Dive
-------------------------

The visitor pattern decouples algorithm implementation from data structure traversal. Each grammar rule labeled with `# label` yields a corresponding `visitLabel()` method in the generated visitor interface.

For example, given this rule:

`expr: expr '*' expr # multiply ;`The generated visitor includes:

`@Override public T visitMultiply(ExprParser.MultiplyContext ctx) { ... }`Within the visitor method, navigate child contexts using `ctx.expr(0)`, `ctx.ID()`, or `ctx.getText()`. Return values propagate up the call stack, enabling natural expression evaluation.

### Evaluation Visitor Implementation

<div class="code-block">```
import java.util.HashMap;
import java.util.Map;

public class EvalVisitor extends ExprBaseVisitor<double> {
  private final Map<String, Double> memory = new HashMap<>();

  @Override public Double visitStoreValue(ExprParser.StoreValueContext ctx) {
    String name = ctx.IDENTIFIER().getText();
    Double value = visit(ctx.expression());
    memory.put(name, value);
    return value;
  }

  @Override public Double visitShowResult(ExprParser.ShowResultContext ctx) {
    Double result = visit(ctx.expression());
    System.out.println("→ " + result);
    return result;
  }

  @Override public Double visitNumberLiteral(ExprParser.NumberLiteralContext ctx) {
    return Double.parseDouble(ctx.NUMBER().getText());
  }

  @Override public Double visitVariableRef(ExprParser.VariableRefContext ctx) {
    String name = ctx.IDENTIFIER().getText();
    return memory.getOrDefault(name, 0.0);
  }

  @Override public Double visitProductExpr(ExprParser.ProductExprContext ctx) {
    Double left = visit(ctx.expression(0));
    Double right = visit(ctx.expression(1));
    return ctx.MULTIPLY() != null ? left * right : left / right;
  }

  @Override public Double visitSumExpr(ExprParser.SumExprContext ctx) {
    Double left = visit(ctx.expression(0));
    Double right = visit(ctx.expression(1));
    return ctx.ADD() != null ? left + right : left - right;
  }
}
</double>

Listeners operate via callback methods triggered during automatic tree traversal. The ParseTreeWalker calls enterXxx() before visiting a node and exitXxx() after visiting its children.

State must be managed externally—typically using ParseTreeProperty<T> to associate computed values with specific parse tree nodes:

public class SymbolCollector extends ExprBaseListener { private final ParseTreeProperty<Double> computedValues = new ParseTreeProperty<>(); private final Map<String, Double> symbolTable = new HashMap<>();

@Override public void exitStoreValue(ExprParser.StoreValueContext ctx) { String id = ctx.IDENTIFIER().getText(); Double val = computedValues.get(ctx.expression()); symbolTable.put(id, val); }

@Override public void exitNumberLiteral(ExprParser.NumberLiteralContext ctx) { computedValues.put(ctx, Double.parseDouble(ctx.NUMBER().getText())); }

@Override public void exitProductExpr(ExprParser.ProductExprContext ctx) { Double lhs = computedValues.get(ctx.expression(0)); Double rhs = computedValues.get(ctx.expression(1)); double result = ctx.MULTIPLY() != null ? lhs * rhs : lhs / rhs; computedValues.put(ctx, result); } }


</div>Visitor vs Listener: When to Use Which
--------------------------------------

**Use Visitor when:**

- You need fine-grained control over traversal order
- Implementing interpreters with conditional execution or loops
- Computing values bottom-up with natural return semantics

**Use Listener when:**

- Building ASTs or symbol tables
- Performing semantic analysis requiring multiple passes
- Processing large inputs where manual recursion management is error-prone

IDE Integration Tips
--------------------

In IntelliJ IDEA:

- Add `antlr-4.13.1-complete.jar` to project libraries
- Install the official *ANTLR v4 Grammar Plugin* for syntax highlighting and navigation
- Configure ANTLR tool location in *Settings → Languages &amp; Frameworks → ANTLR*
- Enable auto-generation on grammar save via *Generate Parser* checkbox

Tags: antlr4 parser-generator compiler-construction visitor-pattern listener-pattern

Posted on Mon, 14 Sep 2026 16:09:53 +0000 by jayd1985