Language Core
ANTLR4 grammar, ASDL AST, builder, visitors, unparser, type system, linter, and error model for PYNE.
This page
Language Core
The language core is the front half of the evaluate contract: source text becomes a typed tree; the tree can be walked, rewritten, dumped, and unparsed without ever entering the bar loop.
Abstract
PYNE treats Pine Script as a formal pipeline, not a host plugin. Lexing and parsing are ANTLR4-driven. The concrete syntax tree is lowered by a hand-written visitor (PinescriptASTBuilder) into ASDL-generated dataclasses. Public helpers in helper.py mirror Python’s ast module: parse, unparse, dump, walk, literal_eval, plus a process-local sha256 LRU parse cache (clear_parse_cache, parse_cache_info). Static tools (linter, type registry, LSP) consume the same tree. Evaluation is out of scope here — see Runtime.
Hard rule: edit grammar only under resource/. Paths under generated/ are regenerated artifacts and must not be hand-edited.
Conceptual model
Rendering…
| Stage | Role | Primary path |
|---|---|---|
| Grammar | Concrete syntax | src/pynescript/ast/grammar/antlr4/resource/*.g4 |
| ASDL | Algebraic node shape | src/pynescript/ast/grammar/asdl/resource/Pinescript.asdl |
| Builder | CST → AST | src/pynescript/ast/builder.py |
| Helper | Public parse/unparse API | src/pynescript/ast/helper.py |
| Nodes | Re-export generated types | src/pynescript/ast/node.py |
Interface surface
Consumers almost always enter through pynescript.ast.helper:
from pynescript.ast.helper import parse, unparse, dump, walk, literal_eval
tree = parse('//@version=6\nindicator("x")\nplot(close)')
print(dump(tree, indent=2))
print(unparse(tree))
Successful parse results are cached by sha256(source) + mode (default ON; PYNE_PARSE_CACHE=0 disables). Treat returned trees as read-only.
Modes:
mode | Entry rule | Root node |
|---|---|---|
"exec" (default) | start_script | Script |
"eval" | start_expression | Expression |
Full page inventory:
| Page | Topic |
|---|---|
| Grammar (ANTLR4) | Resource vs generated; typed names; = vs := |
| ASDL schema | Algebraic AST; codegen |
| Builder | CST visitor → ASDL nodes |
| Helper API | parse, cache, dump, walk, locations |
| Unparser | Precedence-aware source regen |
| Visitor / transformer | Read vs rewrite |
| Type system | Qualifiers, UDT registry |
| Linter | Static rules, codes |
| Error model | SyntaxError, indent, listener |
Internals
src/pynescript/ast/
helper.py # public pipeline + sha256 LRU parse cache
builder.py # PinescriptASTBuilder (_setLocations = direct attrs)
unparser.py # NodeUnparser + Precedence
visitor.py # NodeVisitor
transformer.py # NodeTransformer
collector.py # StatementCollector (annotation pairing)
linter.py # PineLinter
type_system.py # Type*, TypeRegistry, MethodResolver
error.py # SyntaxError / IndentationError
node.py # from grammar.asdl.generated import *
grammar/
antlr4/
resource/ # EDIT HERE: .g4 + *Base.py
generated/ # NEVER HAND-EDIT
error_listener.py
tool/generate.py
asdl/
resource/ # EDIT HERE: Pinescript.asdl
generated/ # NEVER HAND-EDIT
tool/generate.py
Thin wrappers grammar/antlr4/lexer.py, parser.py, visitor.py re-export generated classes so application code imports stable paths.
parse uses a thread-local ANTLR engine and a two-stage SLL then LL strategy; successful trees go into a sha256 LRU (PYNE_PARSE_CACHE, default 128).
Invariants
- Resource is source of truth. Lexer/parser
.g4andPinescript.asdldefine syntax and node shape; generated Python is a build product (committed so install does not require Java). - Round-trip is a test oracle. For the supported surface,
unparse(parse(src))should re-parse and preserve semantics (not byte-identical formatting). - Locations are first-class. Statements and expressions carry
lineno,col_offset, optionalend_*for diagnostics and LSP. - Annotations are not free-floating. Comment tokens on
COMMENT_CHANNELare reified asCommentnodes and attached toScript/FunctionDef/TypeDef/Assignvia kind suffixes (S,F,T,V). - Recursion budget. Deep ternary chains raise the Python recursion limit temporarily during parse (cap ≥ 5000).
- Cached trees are shared by identity. Mutating a
parse()result (transformer,increment_lineno) mutates the LRU entry. Callclear_parse_cache()after intentional mutation, or disable the cache. FunctionDef.returnsis a type spec, not an executable expression. Optional leadingtype_specificationon UDFs/methods is stored there (0.3.9+).
Worked examples
Minimal script:
from pynescript.ast.helper import parse, dump
src = """
//@version=6
indicator("demo")
x = ta.sma(close, 14)
plot(x)
"""
tree = parse(src)
assert tree.__class__.__name__ == "Script"
assert any("version" in a for a in (tree.annotations or []))
print(dump(tree, include_attributes=True, indent=2))
Expression-only eval mode:
from pynescript.ast.helper import parse, literal_eval
expr = parse("1 + 2 * 3", mode="eval")
assert literal_eval(expr) == 7
Failure modes
| Symptom | Likely cause | Where to look |
|---|---|---|
SyntaxError with caret | Lexer/parser reject | Error model |
IndentationError | Bad INDENT/DEDENT from LexerBase | PinescriptLexerBase.py |
Missing visit_* after grammar change | Builder not updated for new rule | Builder |
Hand-edit of generated/ lost on regen | Violated resource-only rule | Grammar |
Full regen breaks builder.py | Parser context accessors changed | Prefer targeted lexer refresh (v6 case study) |