Grammar (ANTLR4)
Resource .g4 grammars, generated artifacts, INDENT/DEDENT, regeneration, and the never-edit-generated rule.
This page
Grammar (ANTLR4)
Concrete syntax for Pine Script in PYNE is defined by a split lexer/parser ANTLR4 grammar plus hand-written base classes that inject Python-like indentation tokens.
Abstract
Two grammars live under src/pynescript/ast/grammar/antlr4/resource/:
PinescriptLexer.g4— keywords, operators, literals (including v6 triple-quoted strings), comments, hidden whitespace.PinescriptParser.g4— statements, expressions, type/enum/function/method declarations, control structures.
Python runtime classes under generated/ are outputs of antlr4 -Dlanguage=Python3. They are committed so end users never need a JDK. Never edit files under generated/ — changes evaporate on the next regeneration and fight the build pipeline.
Hand-written bases (copied into generated/ by the generate tool):
resource/PinescriptLexerBase.py— INDENT/DEDENT, newline elision, multiline string handling.resource/PinescriptParserBase.py— parser superClass hooks.
Conceptual model
Rendering…
Interface surface
Application code should not import generated symbols directly when a wrapper exists:
from pynescript.ast.grammar.antlr4.lexer import PinescriptLexer
from pynescript.ast.grammar.antlr4.parser import PinescriptParser
from pynescript.ast.grammar.antlr4.visitor import PinescriptParserVisitor
from pynescript.ast.grammar.antlr4.error_listener import PinescriptErrorListener
The public parse path is higher level — Helper API — which constructs lexer, token stream, parser, and strips default console error listeners in favor of PinescriptErrorListener.
Entry rules (PinescriptParser.g4)
| Rule | Purpose |
|---|---|
start → start_script | Full script (statements? EOF) |
start_expression | Single expression + optional NEWLINE + EOF |
start_comments | Comment stream only |
Lexer structural facts
| Token / channel | Behavior |
|---|---|
INDENT / DEDENT | Imaginary tokens emitted by PinescriptLexerBase (declared in tokens { }) |
COMMENT | // line comments → COMMENT_CHANNEL (not the default channel) |
HASH_COMMENT | # … line comments remapped -> type(COMMENT) (must not swallow #RRGGBB) |
BACKTICKS | Markdown fence ` remapped to COMMENT (paste noise) |
WS | Spaces/tabs/form-feed → HIDDEN |
UNICODE_NOISE | Curly quotes, bullets, arrows → HIDDEN |
ERROR_TOKEN | Catch-all . for recovery/reporting |
TRIPLE_DQ_STRING / TRIPLE_SQ_STRING | v6 multiline strings, typed as STRING |
LSHIFT / RSHIFT / TILDE / AMP / PIPE / CARET | Bitwise << >> ~ & ` |
Keywords include var, varip, method, export, enum, type, qualifiers const/input/simple/series, and control forms if/for/while/switch/once (August 2026). Many of those words are soft in identifier position (name alternatives) so as = input(...) and once = 3 parse.
Parser structural facts
- Compound vs simple statements. Functions, methods, types, enums, and structure-valued assignments are compound; imports, break/continue, expression statements, and simple assignments terminate with
NEWLINE. - Trailing structures.
a = 0, for i = 0 to nistrailing_structure_statements(comma-separated simples ending in a structure). - Structures are dual-use.
if/for/while/switchappear both as statements and as expressions (structure_expression) — the classic Pine “if returns a value” form.onceis statement-only (once_structureis not invalue_structure);x = once …is a parse error. - Local blocks. Either indented (
NEWLINE INDENT statements DEDENT) or inline single statement. - Library export.
EXPORT?prefixes function/method/type/enum and June-2025export const T name = …compound initializers. - UDF return types.
function_declaration/method_declarationaccept an optional leadingtype_specification(int ilog2(int n) => …). Builder stores it onFunctionDef.returns. - Bitwise layer. Between
andand comparisons:|^&then<</>>, then additive. Unary~isInvert. - Assign vs reassignment (0.3.9).
=on a bare name is initialization (Assignviavariable_declaration). Reassignment of a name is:=(ReAssign).=is reassignment only for attribute / subscript targets (obj.f =,a[i] =). - Left-factored typed names.
variable_declarationisdeclaration_mode? type_specification? name_storewith the typed alternative first soint x = 1does not consumeintas the identifier. Same left-factor onfor_iteratorand parameter lists.
Internals
Paths
| Path | Edit? | Role |
|---|---|---|
src/pynescript/ast/grammar/antlr4/resource/PinescriptLexer.g4 | Yes | Lexer grammar |
src/pynescript/ast/grammar/antlr4/resource/PinescriptParser.g4 | Yes | Parser grammar |
src/pynescript/ast/grammar/antlr4/resource/PinescriptLexerBase.py | Yes | Indentation machine |
src/pynescript/ast/grammar/antlr4/resource/PinescriptParserBase.py | Yes | Parser base |
src/pynescript/ast/grammar/antlr4/generated/** | No | ANTLR output + copied bases |
src/pynescript/ast/grammar/antlr4/tool/generate.py | Tool | Runs antlr4, copies *.py bases |
src/pynescript/ast/grammar/antlr4/error_listener.py | Hand | Maps ANTLR errors → ast.error.SyntaxError |
LexerBase responsibilities
PinescriptLexerBase is not optional sugar; it is part of the language definition:
- Ignore leading / collapse consecutive newlines; ensure trailing newline.
- Suppress newlines inside open
()/[], after operators, and for soft wrap lines whose indent width is not a multiple of four. - Push
INDENT/DEDENTwith tab length and indent length = 4. - Special-case multiline string tokens so wrap indentation inside strings is not mis-tokenized as structure.
Indent mistakes surface as pynescript.ast.error.IndentationError (subclass of the project SyntaxError).
Regeneration
# Project-aware entry (sets -o generated, -lib resource, copies *Base.py)
python -m pynescript.ast.grammar.antlr4.tool.generate
# hatch lint:gen-parser is only the antlr4 CLI (`antlr4 {args}`) — pass flags yourself.
# hatch run lint:gen-parser -- -o …/generated -lib …/resource -listener -visitor -Dlanguage=Python3 …/resource/*.g4
tool/generate.py invokes $(sys.executable)/../antlr4 with -o generated -lib resource -listener -visitor -Dlanguage=Python3 on resource/*.g4, then copies resource/*.py into generated/. Requires a working antlr4 CLI (hatch lint env pulls antlr4-cli). Generated modules are excluded from mypy/ruff strictness in pyproject.toml — do not “lint-fix” them by hand.
Downstream after grammar change
- Regenerate (or selectively refresh lexer — see case study).
- Add/adjust
visit_*methods insrc/pynescript/ast/builder.pyfor new or renamed rules. - If the shape of the language changed (new statement/expression kind), update
Pinescript.asdland regenerate ASDL nodes — ASDL schema. - Smoke-test with
parse/unparseon a minimal first-party snippet before broader regression.
Invariants
- Never hand-edit
generated/. Patches belong inresource/*.g4orresource/*Base.py. - Visitor contract. Generated
PinescriptParserVisitormethod names follow ANTLR rule names; the builder subclass must track renames. - Comments are off the default channel. Annotation logic in
helper._collect_comment_nodesiteratestoken_stream.tokensforCOMMENTtype afterfill(), and keeps only texts containing@.HASH_COMMENT/BACKTICKSare remapped to that same token type. - Indent unit is four spaces. Soft-wrap lines that are not multiples of four are newline-elided, not nested blocks.
Worked examples
Minimal grammar-level mental model
//@version=6
indicator("x")
if close > open
plot(1)
else
plot(0)
Lexer emits NEWLINE, INDENT, DEDENT around the if bodies. Parser builds if_structure → builder yields ast.If with body / orelse statement lists.
v6 triple-quoted strings
Resource lexer rules (illustrative — see live PinescriptLexer.g4):
TRIPLE_DQ_STRING : '"""' ( ~'"' | '"' ~'"' | '""' ~'"' )* '"""' -> type(STRING) ;
TRIPLE_SQ_STRING : '\'\'\'' ( ~'\'' | '\'' ~'\'' | '\'\'' ~'\'' )* '\'\'\'' -> type(STRING) ;
ANTLR quoting pitfall: putting "'''" or '"""' as fragment literals can produce tool errors (quote came as a complete surprise). Factor starters when needed:
fragment TRIPLE_SQ_START: '\'' '\'' '\'';
Content and source indentation inside the triple quotes are preserved literally (no automatic dedent).
Typed names and = vs := (0.3.9)
Pine lets type names occupy identifier positions (int is both a type and a legal NAME alternative via soft keywords). Alternatives must try the typed form first:
variable_declaration
: declaration_mode? type_specification name_store
| declaration_mode? name_store
;
simple_name_initialization: EXPORT? variable_declaration EQUAL expression;
simple_reassignment
: assignment_target_attribute EQUAL expression
| assignment_target_subscript EQUAL expression
| primary_expression COLONEQUAL expression
;
| Source | Node |
|---|---|
x = 1 / var float x = 1.0 / export const int N = 3 | Assign (mode/type/export as present) |
x := 2 | ReAssign |
obj.field = 3 / arr[i] = 4 | ReAssign (attribute/subscript =) |
x += 1 | AugAssign |
Putting name_store before type_specification name_store made int x = 1 parse int as the variable and then fail on x. Do not invert those alternatives.
Case study: targeted lexer refresh (2026-07)
Full regeneration once produced a PinescriptParser.py whose context accessors diverged from what the hand-written builder expected (e.g. missing template_spec_suffix()), breaking even trivial parses. The practical recovery:
- Edit only
resource/grammar. - Generate the lexer in a clean temp directory (avoids nested
src/mirror paths). - Copy only
PinescriptLexer.py(+ refreshedLexerBase.py) intogenerated/. - Leave committed
PinescriptParser.pyand visitors untouched unless prepared to patch the builder in the same change. - Verify immediately:
from pynescript.ast.helper import parse, unparse
ast = parse('indicator("x")\ns = """\nfoo\n bar\n"""\n')
assert "foo" in unparse(ast)
Failure modes
| Failure | Cause | Mitigation |
|---|---|---|
mismatched input after """ | Old lexer ATN without triple rules | Refresh generated lexer from resource |
| Full regen breaks builder | Parser context API drift | Selective copy; or update all visit_* together |
antlr4 not found | CLI not on PATH | Use hatch env or full path to antlr4-cli |
Nested generated/src/pynescript/... junk | Running antlr with wrong -o cwd | Generate from clean temp + explicit -o |
| IndentationError mid-file | Mixed tabs/spaces or wrong nest | Align to 4-space blocks; check soft-wrap rules |
| Silent loss of hand patches | Edited generated/ | Revert; re-apply in resource/ |