ANTLR v4 (ANother Tool for Language Recognition)

repository·dev·Indexed 12 days ago

https://github.com/antlr/antlr4

A powerful parser generator used to build languages, tools, and frameworks by generating parsers, listeners, and visitors from a grammar. It supports 10 target runtimes including Java, C++, C#, Python3, JavaScript, TypeScript, Go, Swift, PHP, and Dart. Version 4.13.2 provides a Java-based tool and corresponding runtimes, with options for deployment via Docker and integration with CMake for C++ projects.

Tokens
90K
Snippets
307
Records
427
Agent score
94%

What's inside ANTLR

  1. What is ANTLR v4

    dev

    ANTLR (ANother Tool for Language Recognition) is a powerful parser generator used for reading, processing, executing, or translating structured text or binary files.

    From a grammar, ANTLR generates:

    1. A parser that builds parse trees.
    2. A listener interface (or visitor) that allows you to easily respond to the recognition of specific phrases within the grammar.
  2. Understand ANTLR 4 code generation and runtimes

    dev

    ANTLR 4 uses a single code generator tool written in Java to produce lexer and parser code for various target languages. You do not need separate generators for each language; instead, you use the central Java-based tool and specify your desired target via command line options or IDE/build system plugins (such as Eclipse, IntelliJ, Visual Studio, or Maven).

    Once the code is generated, you must use the corresponding language-specific Runtime Library to execute the parser and lexer. The available runtime targets include:

    • Java
    • C#
    • Python 3
    • JavaScript
    • TypeScript
    • Go
    • C++
    • Swift
    • PHP
    • Dart
  3. Use the ANTLR 4 Python 3 runtime

    dev
    This runtime provides the necessary Python 3.4+ support to execute parsers and lexers generated by ANTLR 4. For detailed instructions on generating Python code from grammars and using the runtime, refer to the official ANTLR documentation.
  4. Construct an AST from a parse tree

    dev

    If you are building a compiler and require an Abstract Syntax Tree (AST) rather than a concrete parse tree, you have three primary options:

    1. Listener or Visitor: Traverse the parse tree and manually construct your AST nodes.
    2. Grammar Actions: Use embedded actions within your grammar to build the AST during the parsing phase. If you use this method, you can turn off auto-parse-tree construction to save resources.
    3. SSA Form: Generate LLVM-type static-single-assignment form directly.
  5. Use dynamic scoping to pass data between rules

    dev

    ANTLR supports dynamic scoping, allowing a rule to access attributes from its invoking rules (rules higher up in the call chain). This is useful for passing context information (like symbol tables) down through the parse tree.

    Syntax: Use $ruleName::attributeName to access an attribute of a specific invoking rule.

    Important Notes:

    • The rule r must be in the current call chain; otherwise, a runtime exception occurs.
    • This is different from @members fields. A @members field is local to each invocation (lexical scoping), whereas dynamic scoping allows access to the state of the parent rule's context.
    • To walk up the tree manually, you can use $ctx.getParent().
    grammar DynScope;
     
    prog: block ;
     
    block
    	/* List of symbols defined within this block */
    	locals [ List<String> symbols = new ArrayList<String>() ]
    	: '{' decl* stat+ '}'
    	{System.out.println("symbols="+$symbols);} 
    	;
     
    decl: 'int' ID {$block::symbols.add($ID.text);} ';' ;
     
    stat: ID '=' INT ';' 
    	{ if ( !$block::symbols.contains($ID.text) ) { System.err.println("undefined variable: "+$ID.text); } }
    	| block
    	; 
     
    ID : [a-z]+ ;
    INT : [0-9]+ ;
    WS : [ \t\r\n]+ -> skip ;
  6. Use semantic predicates in lexer rules

    dev

    In lexer rules, semantic predicates are used to prune the set of viable rules when the lexer faces ambiguity. Unlike parser predicates (which should be on the left edge of alternatives), lexer predicates are most effective when placed on the right edge of the rule, though they can technically appear anywhere.

    When a predicate evaluates to false, the lexer deactivates that rule for the current input, allowing other rules to be selected.

    Important Constraints:

    • No Side Effects: Lexer predicates cannot depend on side effects from lexer actions within the same rule. This is because actions are only executed after the lexer has positively identified the rule to match, whereas predicates are part of the selection process itself.
    • Action Ordering: Lexer actions must appear after predicates in a rule if the action is intended to be part of the rule's successful match.
    • Execution Timing: Actions are not executed inline during matching; they are collected and executed en masse after the rule is recognized. Therefore, a predicate cannot rely on a variable incremented by an action in the same rule.
    // Example: Using a predicate on the right edge to distinguish a keyword
    ENUM: [a-z]+ {getText().equals("enum")}? 
          {System.out.println("enum!");} 
        ;
    
    ID: [a-z]+ {System.out.println("ID " + getText());} 
        ;
  7. Use Lexical Modes to group rules by context

    dev

    Lexical modes allow you to split a single lexer grammar into multiple sublexers, which is useful for handling different contexts (e.g., switching between standard code and XML tag content).

    • The lexer starts in the default mode.
    • Rules are only matched if they belong to the current active mode.
    • Use the mode command to define a new mode.
    • Modes are only supported in lexer grammars, not combined grammars.
    rules in default mode
    ...
    mode MODE1;
    rules in MODE1
    ...
    mode MODEN;
    rules in MODEN
    ...
  8. Import grammars for reusability

    dev

    The import statement allows a grammar to inherit rules, token specifications, and named actions from another grammar, similar to class inheritance.

    Key Behaviors:

    • Inheritance: The 'main' grammar inherits all rules from imported grammars. Rules in the main grammar override rules in the imported grammars.
    • Merging: ANTLR merges tokens, channels, and named actions (like @members) from imported grammars into the main grammar.
    • Precedence: In a main lexer grammar, rules defined in the main grammar take precedence over imported rules.
    • Import Rules:
      • Lexer grammars can import lexers (including those with modes).
      • Parsers can import parsers.
      • Combined grammars can import parsers or lexers (without modes).
    • Resolution: If multiple imported grammars define the same rule, ANTLR uses the first version found during a depth-first search.
  9. Write target-agnostic grammars to avoid forking for multiple targets

    dev

    When a grammar requires semantic predicates to handle context-sensitive parsing (e.g., Fortran column-sensitive comments or C# template vs. shift operators), you normally have to write predicates in the specific target language (Java, C++, Python, etc.). This usually requires maintaining separate versions of the grammar for every target.

    To avoid this, you can use a target-agnostic format. This involves writing a generic grammar that calls methods on a custom base class, and then using a transformation script to rewrite those calls into the correct syntax for your specific target language (e.g., converting this.method() to self.method() for Python or $this->method() for PHP) before running the ANTLR tool.

  10. Define the structure of an ANTLR grammar file

    dev

    An ANTLR grammar file must be named X.g4 to define grammar X. A grammar consists of a header, followed by optional sections like options, import, tokens, channels, and named actions, and finally the rules.

    Naming Conventions:

    • Parser rules: Must start with a lowercase letter.
    • Lexer rules: Must start with an uppercase letter.

    Grammar Types:

    • Combined Grammar: Defined with grammar Name;. Contains both lexical and parser rules.
    • Parser Grammar: Defined with parser grammar Name;. Contains only parser rules.
    • Lexer Grammar: Defined with lexer grammar Name;. Contains only lexer rules. Only lexer grammars can contain mode specifications or custom channels specifications.
    /** Optional javadoc style comment */
    grammar Name; ①
    options {...}
    import ... ;
     
    tokens {...}
    channels {...} // lexer only
    @actionName {...}
     
    rule1 // parser and lexer rules, possibly intermingled
    ...
    ruleN
  11. Use nongreedy subrules with the wildcard operator

    dev

    By default, EBNF subrules like (...)?, (...)*, and (...)+ are greedy, meaning they consume as much input as possible. To make a subrule nongreedy (consuming the fewest characters necessary to satisfy the rule), append an additional ? suffix. This syntax, .*?, .+?, or ...?, is borrowed from regular expression notation and is available in both the parser and the lexer.

    Common use cases include matching content between delimiters, such as C-style comments or quoted strings.

    // Greedy: matches everything until the end of input
    COMMENT : '/*' .* '*/' ; 
    
    // Nongreedy: matches until the FIRST occurrence of '*/'
    COMMENT : '/*' .*? '*/' -> skip ;
    
    // Nongreedy string matching with escaped quotes
    STRING : '"' ( '\"' | . )*? '"' ;
  12. How Visitors and Listeners work in ANTLR

    dev

    ANTLR provides two ways to traverse the parse tree for analysis:

    Visitors

    • Best for: Computing a single synthesized attribute or when you need explicit control over the order of traversal.
    • Traversal Pattern: Generally implements a post-order tree walk. You must manually call self.visit(child) within your visit methods to continue the walk.
    • Generation: Add the -visitor flag to the antlr4 command.
    • Usage: Inherit from the generated Visitor class and call .visit(tree) on the root node.

    Listeners

    • Best for: Computing both synthesized and inherited attributes.
    • Traversal Pattern: Performs an LR tree traversal. The walker calls an enter method when it first encounters a node, and an exit method after all children of that node have been visited.
    • Generation: Add the -listener flag to the antlr4 command.
    • Usage: Inherit from the generated Listener class and pass your instance to a ParseTreeWalker.walk(listener, tree) call.