Expressions and statements

How the AST walker evaluates operations, calls, control flow, assignments, and user definitions.

This page

Expressions and statements

Abstract

Evaluation is pure visitor dispatch: each ASDL node type maps to a visit_* method on a mixin. Expressions produce values (with Pine na and series rules); statements mutate context, register types/functions, or emit side effects via builtins. This page is the operational semantics of the interpreter path—what runs inside evaluator.visit(tree) each bar.

Conceptual model

Diagram

Rendering…

Interface surface

Expressions (ExpressionEvaluator)

NodeBehavior
BoolOp (and / or)Manual short-circuit: and stops at first falsy (na / 0 / False); or stops at first truthy. Returns a Python bool.
BinOpNA-safe + - * / %; division by zero → na
UnaryOpnot, unary +/- with NA and list map
CompareChained comparisons with NA-safe operators
IfExpTernary: condition, then, else
CallBuiltin dispatch, user functions, methods, multi-dispatch overloads
Tuple / listsLiteral sequences for unpacking and multi-arg returns

Binary ops coerce PineSeries-like objects to .current before operating. List operands broadcast or zip with trailing alignment (see Series & history).

Names (NameEvaluator)

NodeBehavior
NameContext lookup; bare series builtins (na, year, …); else string id (lazy)
AttributeQualified context keys, builtins (strategy.position_size), library modules, UDT fields/methods, array/drawing/matrix method markers, Python getattr fallback
SubscriptSeries history / array / matrix indexing

Critical: qualified names like strategy.opentrades.entry_price are resolved via AST path construction (ast_qualified_name) without evaluating intermediate zero-arg series that would collapse the path to an int.

Statements (StatementEvaluator)

ConstructBehavior
ScriptWalk body; finalize library exports if library(...) active
Assignvar/varip once-per-declaration; const; ordinary assign; tuple unpack; export registration. History-tracked var series get a start-of-bar carry so x[1] is the prior persist.
ReAssign (:=)Update existing binding (and UDT fields)
If / For / WhileStandard control flow; loop variables in context
OnceFire body the first time test is true (omit testtrue) on a confirmed bar, then skip. Unconfirmed realtime ticks may re-run until barstate.isconfirmed commits the fired flag. Does not return a value.
FunctionDefStore callable in context; multi-dispatch overload lists; export; methods tagged __pine_method__. After bar 0 the host walks a hot body that skips Function/Type/Enum/Import (already bound).
TypeDef / EnumDefType registry / enum member dicts
ImportResolve LibraryRegistry → bind alias to LibraryModule
Declarationsindicator / strategy / library via declaration builtins

Function invocation binds parameters into context (with restore of prior bindings on exit) so nested calls and bar-local state interact cleanly. After bar 0, hosts set _pine_defs_locked so re-visiting the script does not re-register overload tables. The locked walk inlines Assign and Expr(Call) (no generic visit frame) and UDF/call-expression history keys use _pine_site_id stamped on the AST.

Calls and multi-dispatch

Method markers returned from attribute evaluation:

MarkerMeaning
("_method_call", instance, name)UDT method
("_array_method", list, name)array.<name>(self, …)
("_ns_method", obj, "label.get_text")Drawing/matrix namespace method
("_ext_method", receiver, name)Free method with receiver as first arg

Overloads with typed first parameters (e.g. matrix.float vs array.label) select via coarse type tags; na receivers deliberately exclude matrix/array/drawing tags so optional UDT fields do not mis-dispatch to tostring on collections.

Internals

PathRole
src/pynescript/ast/evaluator/expressions.pyOps, calls, conditionals
src/pynescript/ast/evaluator/names.pyNames, attributes, subscripts
src/pynescript/ast/evaluator/statements.pyScript structure, assign, defs, import
src/pynescript/ast/evaluator/literals.pyConstants, colors, strings
src/pynescript/ast/evaluator/base.pyContext, math constants, library registry hook
src/pynescript/ast/evaluator/types.pyEvaluatorProtocol typing for mixins

Invariants & edge cases

  1. Context is the single store. Builtins read OHLCV and strategy series from self.context; hosts must keep it coherent each bar.
  2. String fallback names. An unbound Name returns its id string so later attribute/call resolution can still hit the builtin map ("ta.sma" patterns via attribute chains).
  3. Parameter unbind. Missing-before-bind params use a sentinel so unbind pops rather than leaving None ghosts.
  4. na normalization. String "na" / "nan" / "none" may coerce to None at dispatch boundaries for UDT optional args.
  5. User functions re-enter the visitor. Recursive and higher-order patterns are limited by Python stack, not a Pine-specific trampoline.

Worked examples

Ternary and comparison chain

//@version=6
indicator("cmp")
v = close > open ? close - open : open - close
plot(v)

Each operator path is NA-safe: if close or open were missing, comparisons and arithmetic yield na.

User function with series

//@version=6
indicator("fn")
f(x, n) =>
    ta.sma(x, n)
plot(f(close, 14))

f is stored as a Python callable that rebinds x/n and visits the function body AST each call.

UDF series parameters (0.6.2): when a caller passes a scalar (or a host-series current) into a function that then reads s[1], interpret wraps that parameter as a per-call-site history stream. src = close; updown(src) therefore matches compile — the previous-bar sample is the previous argument at that call site, not an empty scalar. Official TradingView® builtin scripts are 0 MISMATCH vs compile on this contract.

once (August 2026)

//@version=6
indicator("once")
var int n = 0
once close > ta.sma(close, 20)
    n := 1
plot(n)

Equivalent to a var bool latch plus if, but cheaper: the structure is not re-evaluated after it fires on a closed bar. On a forming realtime bar (Runtime.run(..., realtime_last_bar=True, realtime_ticks=N)), the latch is not committed until barstate.isconfirmed. Compile is historical (isconfirmed always true).

Intrabar rollback on realtime ticks (0.6.0)

On a forming realtime bar, intermediate ticks snapshot the var scope (context bindings, series currents, declaration set) and restore it after the visit — every tick starts from confirmed state and only the final tick commits. varip names persist across ticks by design. Shallow by design: history pushes, the series-assign map, in-place container mutations, and broker side effects are not rolled back.

Method multi-dispatch sketch

method log(this, string s) => ...
method log(this, float x) => ...

Both register under the same name with __pine_overloads__; call sites pick by receiver/arg type tags.

Failure modes

ErrorMeaning
unexpected type of nodeMissing visit_* for an AST construct
Unsupported binary operatorOp class not in the dispatch table
History series[-1] is naNegative Pine offsets soft-fail to None (not Python wraparound, not a raise)
Wrong overload / destroyed receiverHistorical na string path; now normalized—report residual mismatches as bugs
Stack overflowDeep recursion in user functions

See also