Getting started¶
This walks you from a grammar to a working parser in three steps: generate a parser from your grammar, generate a Python facade, and write a listener subclass.
First, install antlrope and the ANTLR tool.
The examples below use a JSON grammar, which lives in the repository under
examples/json/
(see Examples for the full programs). Swap in your own .g4
grammar and the steps are identical.
Before you start: will this work with your grammar?
Antlrope parses grammars that describe structure — data formats,
config languages, most DSLs and programming languages. It does not run
semantic predicates ({...}?) or embedded actions ({...} code)
that some grammars use, because those are target-language code this runtime
will not execute. If your grammar depends on them, either create a version of your grammar
that does not need them, or stick to the official antlr4-python3-runtime. See
Performance & limitations.
Not sure? Once you have generated a parser (step 1),
antlrope check <parser-module> scans it and reports every semantic
predicate and embedded action by rule — a clean grammar passes with exit
status 0. See Command line.
1. Generate a parser from your grammar¶
Run the stock ANTLR tool with the Python3 target. Nothing here is specific to Antlrope — this is the ordinary ANTLR workflow:
This writes generated/JSONLexer.py and generated/JSONParser.py (plus a couple
of support files). These are the same files you'd use with the pure-Python
runtime.
2. Generate a facade¶
The facade is a small Python base class with one named callback per grammar
rule. Point antlrope at your generated parser module (an importable
dotted path) and give it a name prefix:
That emits json_listener.py containing a JsonEventListener class with:
enterJson/exitJson,enterObj/exitObj,enterPair/ … — a pair of callbacks for every rule in the grammar,visitTerminal(self, token_type, text)andvisitError(self, token_type, text),- token-type constants like
STRING = 10,NUMBER = 11, and - a
walk(text)method that runs everything.
The facade imports your lexer and parser and bakes them in. It finds the lexer by ANTLR's
<Grammar>Lexer / <Grammar>Parser naming (here generated.JSONLexer next to
generated.JSONParser); pass --lexer to antlrope if your lexer module is named differently.
You don't edit this file — you subclass it.
Tip
antlrope rules <parser-module> and antlrope tokens <parser-module> list
the grammar's rule names (as accepted by walk(start_rule=...) and the rule
chunkers) and the facade's token-type constants without opening the
generated file — see Command line.
3. Write a listener¶
Subclass the generated facade and override only the callbacks you need. Here is a complete program that collects every string token in a JSON document:
from json_listener import JsonEventListener # from step 3
class StringCollector(JsonEventListener):
def __init__(self):
self.strings = []
def visitTerminal(self, token_type, text):
if token_type == self.STRING: # STRING is a generated constant
self.strings.append(text)
c = StringCollector()
c.walk('{"name": "ada", "tags": ["x", "y"]}')
print(c.strings)
Running it prints every string literal, quotes included (the callback receives the raw source slice for the token):
That's the whole model:
- Rule callbacks take no arguments. There are no node objects — you keep your
own state. The common pattern is a small stack: push on
enter, pop onexit. Seeexamples/json/to_python.py, which rebuilds a JSON document into Python objects usingenterObj/exitObj,enterArr/exitArr,enterPair, andvisitTerminal. visitTerminal(token_type, text)gives you the token's type (compare against the generated constants) and its text, already sliced from the source.- Override only what you need. The callbacks you define are the only events the C++ side bothers to send to Python — fewer overrides, faster walk.
Scope and source helpers¶
You don't have to track nesting by hand. While a callback runs, the listener knows where it is:
self.depth()— current nesting depth;self.rule_stack()— the open rule names, outermost first;self.current_rule()— the innermost open rule (e.g. insidevisitTerminal, the rule the token belongs to). These count the rules you actually subscribe to — the constructs you track. OverrideenterEveryRule/exitEveryRule(no-op hooks called on every rule) to track the full parse tree instead.self.text()— the source slice of the current event. For a token it's the token text; for a rule it's the rule's whole extent (e.g. the full{...}inexitObj), so you don't have to keep the source around and slicespan().self.token_name(t)/self.rule_name(i)— names for a token type or rule index, handy for logging or a genericvisitTerminal.
So the JSON-style "push on enter, pop on exit" stack is only needed when you're building a result; for "how deep am I / what am I inside / what's the text here" the helpers above already answer it.
Handling parse errors¶
By default ANTLR prints line X:Y ... messages to stderr. The facade captures
them instead, so nothing is printed and you decide what to do. After walk, the
errors from that parse are on self.syntax_errors:
c = StringCollector()
c.walk('{"a": 1 2}') # the stray 2 is a syntax error
for err in c.syntax_errors:
print(f"{err.line}:{err.column}: {err.message}")
syntax_errors is reset on every walk, and is an empty list after a clean
parse.
Where to go next¶
- Migrating from antlr4-python3-runtime — if you already have a
ParseTreeListener, this maps each piece onto the facade. - Parallel parsing — if your input is many
independent pieces (records, definitions, sections), parse them
concurrently with
walk_parallel. - API reference — every callback,
walkoption, source-position helpers, and the raw event buffer for power users. - How it works — optional background on why batching the events makes it fast.