Language Core
ANTLR4 grammar, ASDL AST, builder, visitors, unparser, type system, linter, and error model for PYNE.
This page
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) |