Skip to content

Interpreter: var persistence, series history, and stateful functions#49

Open
FellowTraveler wants to merge 1 commit into
ferranbt:mainfrom
FellowTraveler:pine-v6-semantics
Open

Interpreter: var persistence, series history, and stateful functions#49
FellowTraveler wants to merge 1 commit into
ferranbt:mainfrom
FellowTraveler:pine-v6-semantics

Conversation

@FellowTraveler

@FellowTraveler FellowTraveler commented Jul 5, 2026

Copy link
Copy Markdown

Summary

Note: this PR originally bundled four semantics; it has been split into three independent PRs for easier review (as offered in the original description) — #50 (lexer/parser syntax fixes) and #51 (na semantics) carry the other slices, and this PR now holds the cross-bar state model only. The code is unchanged, just re-partitioned; each PR passes the full workspace suite standalone and any merge order is conflict-free. See the comment below for details.

This PR closes the gap between pinecone and TradingView's cross-bar state model, found while running real trading strategies through pinecone bar-by-bar and validated against TradingView's own strategy-tester trade list.

Heads-up on the diff shape: ~60 of the changed files are regenerated AST JSON snapshots in crates/pine-parser/testdata/ — the only change in each is the new is_var_persistent field. The hand-written changes are confined to pine-ast, pine-parser, pine-interpreter, and the tests.

feat(interpreter): var persistence, series history, and stateful functions

Three related pieces of TV's cross-bar state model:

  • var/varip initialize once. The initializer runs only the first time execution reaches the declaration — once ever, not per bar or per loop iteration (var in the reference). Tracked by declaration execution rather than name existence, so var close = 10 correctly shadows a host-injected builtin series. A new AST field is_var_persistent distinguishes var x = e from plain x = e (which re-evaluates every bar); the parser threads it and the AST JSON snapshots are regenerated (the only snapshot change is the new field).
  • Series history for user variables. name[n] lookback now works on user-computed series via a per-variable history. Plain declarations push their previous value on re-execution; := reassignment of a var variable pushes before evaluating the RHS, so acc := acc[1] + 1 sees the previous bar's committed value — this ordering was the single root cause of a systematic one-bar lag in every recursive series (Heikin-Ashi etc.) when we compared against TV. Variables without tracked history fall through to the existing Series historical provider, so builtin series indexing (close[1]) behaves exactly as before.
  • Functions are stateful. Pine function locals persist across calls, so a var counter inside a function accumulates across bars and o[1] inside a function sees the previous bar's local value. Locals are kept per function; parameters are always freshly bound.

Tests: three multi-bar tests in tests/lib.rs (a var counter persists across bars; history is pushed before RHS eval; a function-local var accumulates across calls) and a new testdata/loops/var_init_once_in_loop.pine pinning once-ever initialization inside a loop.

Note on testdata/loops/nested_while.pine

That test used var j = 0 as the inner loop counter. Under correct var semantics the initializer runs once ever, so j stays 2 after the first outer iteration and the expected 3 (which encoded re-initialization per iteration) becomes unreachable. To preserve the test's intent — nested loop mechanics — the inner counter is now a plain declaration (j = 0), and the new var_init_once_in_loop.pine documents the once-ever behavior explicitly with the same shape.

Validation

Beyond the tests in this PR, the combined change set (this PR plus #50 and #51) was validated externally: we run full Pine strategies through pinecone one bar at a time and diff the emitted entries/exits against TradingView's own strategy-tester CSV export for the same symbol/timeframe. With all of them in place, two real strategies match TV 100% — 56/56 signals on a 4H strategy and 21/21 on a daily strategy, including exit-reason order comments — over their full comparison windows.

Happy to squash or adjust any of this to fit the project's conventions.

@FellowTraveler

Copy link
Copy Markdown
Author

Pushed one follow-up commit (02188cf): ==/!= with an na operand now yield na like the other comparison operators. They were missed by the na-propagation commit because they route through values_equal (structural equality) rather than to_number(), so 1 != na returned true — e.g. dayofweek != dayofweek[1] fired on the first bar of a series. values_equal itself is unchanged (switch keeps structural matching). Adds crates/pine-interpreter/tests/na_comparison.rs (4 tests); workspace fmt/clippy/test clean.

…tions

Implements TradingView Pine semantics for variable state across bars:

- var/varip declarations initialize only the FIRST time execution
  reaches them (once ever, not per bar or per loop iteration), tracked
  by declaration execution (var_decls_initialized) rather than name
  existence so 'var close = 10' can shadow a host-injected builtin.
  New AST field is_var_persistent distinguishes 'var x = e' from plain
  'x = e' (re-evaluated every bar); parser threads it and AST JSON
  snapshots are regenerated.
- user_series_history: per-variable history enabling Pine's name[n]
  lookback on user-computed series. Plain declarations push their old
  value on re-execution; := reassignment of a var-persistent variable
  pushes BEFORE evaluating the RHS so acc[1] inside the RHS sees the
  previous bar's committed value. Variables without tracked history
  fall through to the existing Series historical provider (builtin
  series like close[1] behave exactly as before).
- Pine functions are stateful: all function-local variables persist
  across calls (function_local_state), so a 'var' counter inside a
  function accumulates across bars; locals are restored per function
  and parameters are always freshly bound. Method-local var
  declarations survive method calls the same way.

Tests: multi-bar state tests (var persists across bars; history pushed
before RHS eval; function-local var accumulates across calls), new
var_init_once_in_loop.pine pinning once-ever init inside a loop, and
nested_while.pine's inner counter made a plain declaration to preserve
that test's intent under the new (correct) var semantics.
@FellowTraveler FellowTraveler changed the title Pine v6 semantics: na propagation, var persistence + series history, line wrapping, parser dedent fix Interpreter: var persistence, series history, and stateful functions Jul 7, 2026
@FellowTraveler

Copy link
Copy Markdown
Author

Following up on the "happy to split" offer in the original description: I've split this PR into three independent pieces so each can be reviewed on its own — the original 72-file diff was mostly regenerated AST snapshots and made the substantive changes harder to see.

No code changed in the split — the commits were re-partitioned as-is, and the three branches combined reproduce the previous head (02188cf) byte-for-byte. Each PR compiles and passes the full workspace suite standalone (fmt/clippy clean), and the three are mutually conflict-free in any merge order, so feel free to take them in whatever order suits.

@ferranbt

ferranbt commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Hey @FellowTraveler. I did not get notified for the contributions. I will take a look at them. Thank you very much! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants