diff --git a/pineforge_codegen/codegen/visit_expr.py b/pineforge_codegen/codegen/visit_expr.py index ab0dbc2..ece266c 100644 --- a/pineforge_codegen/codegen/visit_expr.py +++ b/pineforge_codegen/codegen/visit_expr.py @@ -995,6 +995,54 @@ def _visit_subscript(self, node: Subscript) -> str: if cpp_t not in ("double", "int", "bool"): cpp_t = "double" member = self._inline_history_member("hist_call", node) + ta_site = self._get_ta_site(node.object) + ta_name = ( + self._ta_name_from_site(ta_site) + if ta_site is not None + else "" + ) + if ( + ta_site is not None + and ta_name in {"highest", "lowest"} + and self._ta_site_uses_precalc(ta_site) + ): + # Static chart Highest/Lowest sites already advance in the + # full-bar precalculation pass, including when the source call + # sits behind a Pine-v6 lazy boolean edge. Their direct history + # operator must use that same chart-bar clock. Advancing only + # ``_hist_call`` when the lazy RHS is reached changes ``[1]`` + # into "previous evaluation" and can permanently miss a first + # qualifying signal (Filter V2's source-bound v6 oracle). + # + # Keep evaluation lazy: this branch only reads the immutable + # precalculated result when the expression is reached. Dynamic + # runs, UDF sites, and non-precalculated calls retain the + # established call-local history fallback below. + ta_mem = self._ta_member_name(ta_site) + precalc = f"_precalc_{ta_mem}" + return ( + f"([&]() -> {cpp_t} {{ " + f"if (_use_precalc) {{ " + f"auto _pf_hist_offset_raw = ({idx}); " + f"if (is_na(_pf_hist_offset_raw)) return na<{cpp_t}>(); " + f"long double _pf_hist_offset_numeric = " + f"static_cast(_pf_hist_offset_raw); " + f"if (bar_index_ < 0 || _pf_hist_offset_numeric < 0.0L || " + f"_pf_hist_offset_numeric > " + f"static_cast(bar_index_)) " + f"return na<{cpp_t}>(); " + f"std::size_t _pf_hist_offset = " + f"static_cast(_pf_hist_offset_numeric); " + f"std::size_t _pf_hist_bar = " + f"static_cast(bar_index_) - _pf_hist_offset; " + f"if (_pf_hist_bar >= {precalc}.size()) " + f"return na<{cpp_t}>(); " + f"return {precalc}[(std::size_t)_pf_hist_bar]; }} " + f"{cpp_t} _hv = ({inner}); " + f"if (history_advances_new_bar()) {member}.push(_hv); " + f"else {member}.update(_hv); " + f"return {member}[(int)({idx})]; }}())" + ) return ( f"([&]() -> {cpp_t} {{ " f"{cpp_t} _hv = ({inner}); " diff --git a/tests/test_codegen_validation_fixes.py b/tests/test_codegen_validation_fixes.py index 0cc6620..8530789 100644 --- a/tests/test_codegen_validation_fixes.py +++ b/tests/test_codegen_validation_fixes.py @@ -560,6 +560,48 @@ def test_ta_precalc_lazy_scope_routes_recursive_ema_only(): assert len(re.findall(r"_use_precalc \? _precalc__ta_sma_", cpp)) >= 2 +def test_precalculated_extrema_direct_history_uses_chart_bar_clock(): + """``ta.highest/lowest(...)[k]`` must index their precalculated series. + + The surrounding Pine-v6 boolean remains lazy. Only the already-computed + direct-call history changes from a sparse evaluation clock to the chart-bar + clock used by the same TA site's ``_precalc_*`` values. + """ + cpp = _cpp( + "pred = close > open\n" + "lo = pred and close < ta.lowest(low, 20)[1]\n" + "hi = pred and close > ta.highest(high, 10)[2]\n" + "plot((lo or hi) ? 1 : 0)" + ) + + assert "pred &&" in cpp + assert "std::vector _precalc__ta_lowest_1" in cpp + assert "std::vector _precalc__ta_highest_2" in cpp + assert "static_cast(bar_index_) - _pf_hist_offset" in cpp + assert "_pf_hist_offset_numeric < 0.0L" in cpp + assert "static_cast(bar_index_)" in cpp + assert "return _precalc__ta_lowest_1[(std::size_t)_pf_hist_bar]" in cpp + assert "return _precalc__ta_highest_2[(std::size_t)_pf_hist_bar]" in cpp + # Dynamic/non-precalculated execution retains the rollback-safe synthetic + # history fallback rather than silently becoming eager. + assert cpp.count("if (history_advances_new_bar()) _hist_call_") >= 2 + + +def test_nonprecalculated_extrema_direct_history_keeps_evaluation_clock(): + """Unsafe extrema inputs must retain their call-local history clock.""" + cpp = _cpp( + "src = close\n" + "pred = close > open\n" + "signal = pred and close > ta.highest(src, 20)[1]\n" + "plot(signal ? 1 : 0)" + ) + + assert "std::vector _precalc__ta_highest" not in cpp + assert "_pf_hist_bar" not in cpp + assert "_ta_highest_1.compute(src)" in cpp + assert "if (history_advances_new_bar()) _hist_call_" in cpp + + def test_recursive_ema_lazy_edges_preserve_eager_operand_positions(): """Only OR RHS / ternary arms opt out; eager positions still precalc.""" cpp = _cpp(