Skip to content

Interpreter: lazy and/or — skip right-operand evaluation when the left decides (stacked on #51)#53

Draft
FellowTraveler wants to merge 3 commits into
ferranbt:mainfrom
FellowTraveler:pine-lazy-and-or
Draft

Interpreter: lazy and/or — skip right-operand evaluation when the left decides (stacked on #51)#53
FellowTraveler wants to merge 3 commits into
ferranbt:mainfrom
FellowTraveler:pine-lazy-and-or

Conversation

@FellowTraveler

Copy link
Copy Markdown

Important

Stacked on #51 — do not merge before it. This change calls to_bool(), introduced by #51's na-propagation commit, so the branch necessarily contains #51's two commits and they appear in this diff. Only the last commit (lazy and/or) is new here — +13 lines in the interpreter plus a test file. Opened as a draft; once #51 merges I'll rebase onto main (the diff collapses to just that commit) and mark it ready. Independent of #49, #50, and #52.

Summary

Completes the TV-semantics series (#49/#50/#51/#52), same origin: found by diffing pinecone's output against TradingView's strategy tester on real strategies.

feat(interpreter): lazy and/or — skip right-operand evaluation when the left decides

Pine's and/or short-circuit: a false left operand decides and, a true left operand decides or, and in those cases the right operand is not evaluated at all — side effects inside it (function calls, stateful built-ins such as ta.crossover) must not run.

This was validated against TradingView strategy-tester ground truth: a strategy calling ta.crossover inside an or right-operand only advances that call's internal cross state on bars where the operand is actually evaluated. We arbitrated this empirically with a 15-year BTCUSD trade-list export (~1,900 trades) on a strategy whose entry logic has exactly this shape — eager evaluation diverges from TV within the first months; lazy evaluation reproduces TV's trades at the data-noise ceiling.

Results are unchanged in every case — #51's three-valued arms still apply when the left operand does not decide (false absorbs na in and, true absorbs na in or, and an na left operand still requires the right operand's value).

Tests: new crates/pine-interpreter/tests/lazy_eval.rs probes evaluation with an undefined variable in the right operand (evaluated → UndefinedVariable error; skipped → success) and pins all three-valued result combinations.

Note: skipping evaluation means builtin call sites no longer execute on every bar, which is what makes execution-order state keying unsound for hosts — #52 (independent of this PR) provides the stable call-site identity that hosts can key on instead.

Arithmetic, comparison, and unary operators previously raised
'Expected number' type errors when an operand was na; conditions
treated na as an error too, and NaN was truthy (n != 0.0 is true for
NaN in IEEE 754). Pine v6 semantics:

- to_number()/to_bool() return Option (None = na) so callers can
  propagate na; as_number()/as_bool() keep their signatures for
  builtins (na -> NaN / false).
- Arithmetic and ordering comparisons with na yield na.
- and/or use three-valued logic: false absorbs na, true absorbs na;
  otherwise na.
- if/while/ternary conditions use truthy_for_condition(): na takes the
  false/else branch.
- NaN is no longer truthy in numeric->bool coercion.
- The na keyword doubles as a function: na(x) tests whether x is na.

New integration script basics/na_propagation.pine pins all of the
above (arithmetic, comparison, three-valued and/or, na(), na-condition
if-expression).
Follow-up to the na-propagation pass: == and != were missed because they
route through values_equal (structural equality) rather than to_number(),
so a comparison with na leaked a structural bool ('1 != na' -> true,
'na == na' -> true). Pine semantics: any comparison with an na operand
yields na (falsy in conditions); testing for na requires the na()
function. Reference: TradingView Pine Script v6 User Manual, 'na value'
(https://www.tradingview.com/pine-script-docs/language/type-system/#na-value).

The practical failure mode is first-bar seeding: series like
'dayofweek != dayofweek[1]' must be na (falsy) on the first bar, where
[1] is na — with structural equality they evaluate true and fire
new-day/new-session logic one bar early.

values_equal itself is unchanged: switch-pattern matching still uses
structural equality, matching Pine's switch behavior.

New integration test crates/pine-interpreter/tests/na_comparison.rs pins
eq/neq with na operands (na), na == na (na), non-na comparisons (bool),
and the falsy-in-if regression.
…eft decides

Pine's and/or short-circuit: a false left operand decides 'and', a true
left operand decides 'or', and in those cases the right operand is not
evaluated at all — side effects inside it (function calls, stateful
built-ins such as ta.crossover) must not run. Validated against
TradingView strategy-tester ground truth: a strategy calling
ta.crossover inside an 'or' right-operand only advances that call's
internal cross state on bars where the operand is actually evaluated.

Results are unchanged in every case — the existing three-valued arms
still apply when the left operand does not decide (false absorbs na in
'and', true absorbs na in 'or', and an na left operand still requires
the right operand's value).

New integration test crates/pine-interpreter/tests/lazy_eval.rs probes
evaluation with an undefined variable in the right operand (evaluated →
UndefinedVariable error; skipped → success) and pins all three-valued
result combinations.
@@ -0,0 +1,62 @@
// This Source Code Form is subject to the terms of the Mozilla Public

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you write this as testdata tests?

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