Skip to content

fix: evaluate conditionally-reached security ta.* lazily to match Pine short-circuit semantics - #110

Merged
luisleo526 merged 1 commit into
mainfrom
ship/lazy-ta-eval
Aug 13, 2026
Merged

fix: evaluate conditionally-reached security ta.* lazily to match Pine short-circuit semantics#110
luisleo526 merged 1 commit into
mainfrom
ship/lazy-ta-eval

Conversation

@luisleo526

Copy link
Copy Markdown
Contributor

Gate

pr-gate PASS — engine corpus zero regressions, scraped entering excellent+strong >= leaving.

Verdict: /Users/haoliangwen/code/pineforge-lab/pr-gate-verdict.json

🤖 Generated with Claude Code

Pine evaluates `ta.*` LAZILY. A call sitting in an untaken ternary branch or
in a short-circuited `and`/`or` operand is not evaluated on that bar, so its
series state does not advance. `_emit_security_evaluators` hoisted EVERY TA
site collected from a `request.security` expression into an unconditional
prologue:

    auto _secval_9  = slot_is_new ? _sec0__ta_ema_10.compute(bar.close) : ...
    auto _secval_10 = ...
    ...
    _req_sec_0 = (_secval_9 > _secval_10 && close > _secval_11) ? 1 : ...

so every site advanced on every HTF bar regardless of whether Pine reached
it. On the common MTF-trend shape

    request.security(sym, tf,
        (ta.ema(close,20) > ta.ema(close,50) and close > ta.ema(close,200)) ? 1
      : (ta.ema(close,20) < ta.ema(close,50) and close < ta.ema(close,200)) ? -1 : 0)

the four conditionally-reached EMAs desynchronised against TradingView within
the first weeks and never recovered.

`_security_lazy_ta_keys` classifies each `(ta_index, binding_signature)` by
where Pine actually reaches it, and conditionally-reached sites are dropped
from the eager prologue. `_build_security_expr` already had an inline
`(security_series_slot_is_new(N) ? m.compute(a) : m.recompute(a))` fallback for
sites absent from `ta_results`; emitting there puts the advance inside the
expression, where C++'s own `&&` / `||` / `?:` short-circuit fires it on
exactly the bars Pine does. The relational lowering's `_pna_l` / `_pna_r`
temporaries keep operand evaluation left-to-right, and `recompute()` restores
the pre-advance snapshot before recomputing, so a slot first reached on a
later chart bar still advances exactly once.

The classifier is deliberately conservative — anything not provably
single-reach AND conditional keeps the eager hoist:

  * A site reached more than once stays hoisted. Sharing one `_secval_*` is
    what bounds it to a single advance per bar; two inline copies would
    advance it twice. (Distinct helper call sites already get distinct TA
    variant members, so each variant is classified on its own.)
  * Laziness does not propagate through a global binding. In the requested
    context that global is its own unconditional top-level statement and
    evaluates on every HTF bar however it is read — this is what keeps
    `t0`-shaped chart code exact.
  * History-offset sites (`ta.ema(close,55)[1]`) keep the committed
    `_secval_*` their per-bar Series push reads.
  * Securities that rebind mutable globals, and multi-statement helper calls,
    bail entirely: both lower through statement emitters that consume
    `ta_results` outside this expression.

The chart-context path is untouched — it never hoisted, so its `?:` / `&&`
already short-circuit the compute inline.

Evidence: on mylivingedge-mylivingedgefx-panel the regenerated evaluator is
semantically identical to a hand-patched lazy reference and produces a
byte-identical tape; against the TV deep-backtest tape (1432 entries, 13
months, +8h) it goes from 1236/1432 matched with count delta +165 to
1432/1432 with count delta 0. Regenerating all 416 standard-corpus slugs
old-vs-new: 413 byte-identical (including every one of the 51 other slugs
with conditional CHART ta.*, and security-only slugs such as
thulashimohanr-prev-day-week-levels-or-vwap-strategy); the 3 that change are
the only ones with ta.* reachable only through a security-expression
conditional. The two besides mylivingedge were re-run end to end and their
tapes are byte-identical to the pre-fix tapes (heneralmomo25-selda-97ma
19/19, remarkablefreddy-scale-strat-trade-plan-wedge-trendlines 8396/8396).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@luisleo526
luisleo526 merged commit 014ec26 into main Aug 13, 2026
9 checks passed
@luisleo526
luisleo526 deleted the ship/lazy-ta-eval branch August 13, 2026 00:05
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.

1 participant