[ASDL Schema]

Algebraic AST definition in Pinescript.asdl, generated dataclasses, and how schema changes flow into the pipeline.

ASDL Schema

The abstract syntax tree is not free-form dicts. It is an algebraic data type declared in ASDL and materialized as Python dataclasses.

Abstract

src/pynescript/ast/grammar/asdl/resource/Pinescript.asdl defines the Pinescript module: roots (Script, Expression), statements, expressions (including control structures that are also expressions), operators, params, args, cases, and comments. Codegen writes grammar/asdl/generated/PinescriptASTNode.py. src/pynescript/ast/node.py re-exports that module so the rest of the codebase imports from pynescript.ast import node as ast (or from pynescript.ast.node import Script).

ASDL is the contract between builder (produces nodes), evaluator/LSP (consume nodes), and unparser (serializes nodes). Changing a field without regenerating and updating all three is a schema break.

Conceptual model

Each product sum type becomes a Python class hierarchy under AST with:

  • _fields — logical children (walked by iter_fields / visitors)
  • _attributes — location metadata (lineno, col_offset, end_lineno, end_col_offset) where declared

Interface surface

Module roots (mod)

ConstructorFieldsUse
Scriptstmt* body, string* annotationsFull file (mode="exec")
Expressionexpr bodySingle expression (mode="eval")

Statements (stmt)

ConstructorNotable fields
FunctionDefname, param* args, body, method?, export?, annotations
TypeDefUDT type Name body (fields + methods as stmts)
EnumDefenum Name members
Assigntarget, optional value/type/mode (Var/VarIp), export?, annotations
ReAssign:= reassignment
AugAssign+= … ops
Importnamespace/name/version, optional alias
ExprExpression statement
Break / ContinueLoop control

All stmt and most other attributed products carry location attributes.

Expressions (expr)

Operators and primaries:

  • BoolOp, BinOp, UnaryOp, Conditional (? :), Compare
  • Call, Constant, Attribute, Subscript, Name, Tuple
  • Structures as expressions: ForTo, ForIn, While, If, Switch — same shapes can appear as values (Pine’s expression-oriented control flow)
  • Qualifyconst/input/simple/series applied to a type/expression
  • Specialize — generic specialization form (value + args)

Auxiliaries

ProductRole
decl_modeVar | VarIp
type_qualConst | Input | Simple | Series
expr_contextLoad | Store
paramFunction/method parameter
argCall argument (optional keyword name)
caseSwitch arm (pattern?, body)
cmnt / CommentComment with value and kind (annotation classification)

Operators are empty product constructors (Add, And, Eq, …) used as tags — the unparser maps class names back to tokens.

Importing nodes

from pynescript.ast import node as ast

script = ast.Script(body=[
    ast.Expr(value=ast.Call(
        func=ast.Name(id="plot", ctx=ast.Load()),
        args=[ast.Arg(value=ast.Name(id="close", ctx=ast.Load()))],
    ))
])

Prefer constructing via parse() unless synthesizing trees for tests.

Internals

Paths

PathEdit?
src/pynescript/ast/grammar/asdl/resource/Pinescript.asdlYes — schema source of truth
src/pynescript/ast/grammar/asdl/generated/PinescriptASTNode.pyNo — regenerated
src/pynescript/ast/grammar/asdl/tool/generate.pyGenerator entry
src/pynescript/ast/node.pyThin re-export (from .grammar.asdl.generated import *)

Generated class shape

Excerpt of generated form (do not edit this file; shown for orientation):

@_dataclasses.dataclass
class Script(mod):
    body: list[stmt] = field(default_factory=list)
    annotations: list[string] = field(default_factory=list)
    _fields: ClassVar[list[str]] = ["body", "annotations"]

stmt base injects location _attributes. Hashability is forced to object.__hash__ so nodes can live in sets/maps by identity when needed.

Regeneration

When the schema changes:

# Project convention (pyasdl / asdl tool — see asdl/tool/generate.py)
pyasdl src/pynescript/ast/grammar/asdl/resource/Pinescript.asdl \
  -o src/pynescript/ast/grammar/asdl/generated

Then update, in the same change set:

  1. builder.py construction sites
  2. unparser.py visit_* methods
  3. Evaluator expression/statement handlers
  4. Any isinstance checks in LSP features

Dual nature of structures

ASDL places If, ForTo, ForIn, While, Switch under expr, while the builder often wraps structure-as-statement under Expr(value=…). Annotation collection (StatementCollector) treats assign/reassign/augassign/expr that hold structure values specially so nested statements remain visible for //@… pairing.

Invariants

  1. Schema first. New language constructs need an ASDL constructor (or a clear encoding into an existing one) before evaluator semantics.
  2. Fields vs attributes. Visitors walk _fields only. Location is metadata; missing locations are fillable via fix_missing_locations.
  3. Optional flags as int?. method and export are integer flags (truthy when set), not booleans — match builder assignments (export = 1).
  4. Generated file is not a style playground. Formatting/lint of PinescriptASTNode.py is overwritten on regen.

Worked examples

Map ASDL to a short script

//@version=5
f(x) => x + 1
a = f(close)

Approximate tree:

Script(
  annotations=["//@version=5", ...],
  body=[
    FunctionDef(name="f", args=[Param(name="x")], body=[Expr(BinOp(...))]),
    Assign(target=Name("a", Store), value=Call(...)),
  ],
)

Dump fields programmatically

from pynescript.ast.helper import parse, iter_fields

tree = parse("x = 1")
for name, value in iter_fields(tree):
    print(name, type(value).__name__)

Failure modes

FailureCause
AttributeError on new fieldSchema regenerated but consumer not updated
Builder constructs wrong productASDL and grammar out of sync
Unparser omits new nodeMissing visit_NewNodegeneric_visit may emit nothing useful
Hand-edit of generated nodes lostEdited generated/ instead of .asdl

See also