AST Builder
PinescriptASTBuilder: ANTLR parse-tree visitor that constructs ASDL nodes, locations, and comment kinds.
This page
The builder is the semantic boundary between ANTLR’s concrete parse tree and PYNE’s ASDL abstract tree.
Abstract
src/pynescript/ast/builder.py defines three cooperating mixins:
PinescriptASTLocator— mapsParserRuleContext.start/stoptokens tolineno/col_offset/end_*.PinescriptCommentParser— classifies//…text into annotation kinds (@=S,@0F,#, plain//, …).PinescriptASTBuilder— subclasses generatedPinescriptParserVisitor, implementsvisit_*for every meaningful rule, returns ASDL nodes.
helper._parse constructs a builder, runs builder.visit(rule_context), then (in exec mode) uses StatementCollector + comment tokens to attach annotations. The builder itself does not walk the default token channel for comments; that is a post-pass.
Conceptual model
Rendering…
Interface surface
You rarely instantiate the builder directly; parse() does. For tools that already hold a parse tree:
from pynescript.ast.builder import PinescriptASTBuilder
from pynescript.ast.grammar.antlr4.parser import PinescriptParser
# ... construct parser, parse to ctx ...
node = PinescriptASTBuilder().visit(ctx)
Locator API
The live path writes attributes directly. Do not describe _setLocations as a dict apply — that was an older micro-opt leftover.
| Method | Role |
|---|---|
_setLocations(node, ctx) | Hot path. Sets node.lineno / col_offset / end_* from ctx.start / ctx.stop (no intermediate dict). |
_getLocations(ctx) | Unused helper that still returns a four-key dict. Not called by visit methods. |
End column calculation accounts for embedded newlines in the stop token text. The builder instance is stateless; helper._SHARED_BUILDER reuses one object for every parse.
Comment kinds
_parseComment returns (kind, parts):
| Pattern | Kind prefix | Suffix | Example |
|---|---|---|---|
//@version = 5 | @= | S (script) | version assignment |
//@description … / //@strategy_alert_message … | @0 | S | script description / alert |
//@function / //@returns | @0 | F | function docs |
//@type | @0 | T | type docs |
//@variable | @0 | V | variable docs |
//@param name … | @1 | F | param docs |
//@field name … | @1 | T | field docs |
//# region / endregion | # | — | editor regions |
other // | // | — | ordinary comment |
helper._add_annotations filters by suffix when attaching to nodes.
Store context
_set_store_ctx walks assignment targets (Name, nested Tuple) and sets ctx = Store() so load/store distinction is available to later passes (Python-ast style).
Representative visit methods
| Rule visitor | Emits |
|---|---|
visitStart_script | Script(body) |
visitStart_expression | Expression(body) |
visitFunction_declaration / visitMethod_declaration | FunctionDef (method=1 for methods; optional returns= type spec) |
visitType_declaration | TypeDef |
visitEnum_declaration | EnumDef |
visitVariable_declaration | Assign shell (target/type/mode); parent fills value |
visitSimple_name_initialization / visitCompound_name_initialization | Assign (export=1 if EXPORT) |
visitSimple_reassignment / visitCompound_reassignment | ReAssign (:=, or = on attr/subscript) |
visit*_*_augassignment | AugAssign |
visitIf_structure / visitFor_* / visitWhile_* / visitSwitch_* / visitOnce_structure | Structure expressions (once is statement-only) |
visit*_*_expression (disjunction → … → primary) | Operator trees with correct associativity |
visitLiteral_* | Constant (numbers via ast.literal_eval, strings, bools, colors) |
visitPrimary_expression_call | Call + Arg list (positional/keyword) |
Expression climbing follows the grammar’s layered rules (conditional → disjunction → conjunction → bitwise or/xor/and → equality → inequality → shift → additive → multiplicative → unary (~/not/+/-) → primary).
returns is visited from ctx.type_specification() and must not be evaluated as a value by later engines.
Internals
Path
- Implementation:
src/pynescript/ast/builder.py - Visitor base:
src/pynescript/ast/grammar/antlr4/visitor.py→ generatedPinescriptParserVisitor - Node types:
src/pynescript/ast/node.py - Annotation pairing:
src/pynescript/ast/collector.py+helper._add_annotations
Statement list flattening
visitStatements flattens each statement visitor’s result: some rules return a list (simple multi-statement lines joined by commas), so the body is always a flat list[stmt].
Export flag
When EXPORT is present on library declarations or name initialization, the builder sets export = 1 on the corresponding node (integer flag per ASDL int?).
Assign vs ReAssign
visitVariable_declaration always builds Assign (optional type / var/varip mode). = after a bare or typed name is initialization. visitSimple_reassignment emits ReAssign for obj.f = / a[i] = / name := — never for name =. See Grammar.
Field / enum members
Field definitions become Assign-like statements inside TypeDef.body (with optional VarIp mode). Enum members become assignment-shaped stmts under EnumDef.body. The unparser special-cases method members of types when pretty-printing.
Invariants
- Every constructed stmt/expr that has a source span should receive
_setLocations(direct attribute writes) so LSP diagnostics andget_source_segmentwork. - Visitor method names must track generated rule names. Renaming a parser rule without updating the builder is a silent
generic_visitfallback (wrong or empty trees). - Do not edit
builder.py.bak. Live builder isbuilder.pyonly (stale backups are forbidden under project policy). - Grammar and builder co-evolve. A resource-only grammar change that adds rules is incomplete until
visit_*exists.
Worked examples
What x = close + 1 becomes
Rough structure after build:
Assign(
target=Name(id="x", ctx=Store()),
value=BinOp(
left=Name(id="close", ctx=Load()),
op=Add(),
right=Constant(value=1),
),
)
Export const initialization
Grammar: EXPORT? variable_declaration EQUAL structure_expression
Builder sets assign.export = 1 when ctx.EXPORT() is present — required for library export of typed constants.
Typed UDF (FunctionDef.returns)
int ilog2(int n) =>
// ...
visitFunction_declaration stores the leading type_specification on FunctionDef.returns (here a Name(id="int")). Methods do the same with method=1. The unparser prints it back as a leading type.
Debugging a missing visitor
from pynescript.ast.helper import parse, dump
print(dump(parse(your_snippet), indent=2))
If a subtree is None or a raw unexpected type, find the rule in PinescriptParser.g4 and the matching visitRule_name in builder.py.
Failure modes
| Symptom | Cause |
|---|---|
AttributeError: '…Context' object has no attribute '…' | Full parser regen changed accessors; builder outdated |
| Locations all zero / missing | Forgot _setLocations on new visit method |
| Annotations not attached | Comment kind suffix wrong, or statement not yielded by StatementCollector |
| Tuples not assignable | Store context not propagated through Tuple.elts |
| Structure expression lost | Wrong rule branch between statement vs expression path |