From 66612eda9ea834e872f48dbe3689c500b1e22cb5 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Tue, 15 Sep 2026 09:45:30 +0800 Subject: [PATCH] Lower price-less strategy.exit to the bracket-cancel command and read the first-tick predicate through its accessor (R4-D W5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lower price-less strategy.exit to strategy_exit_cancel_bracket with from_entry and comment preservation. Require PINEFORGE_HAS_NATIVE_LOWERING_V1 in every generated translation unit, route barstate.isnew through is_first_tick(), and regenerate generated-output goldens and exact byte pins. Contract: §4; §2 P13; amendment A22. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016iF71UAEuQ7F6eqWgecAHm --- pineforge_codegen/codegen/emit_top.py | 5 +++ pineforge_codegen/codegen/visit_call.py | 4 +- pineforge_codegen/codegen/visit_expr.py | 2 +- tests/golden/matrix_eigen_pca.cpp | 3 ++ tests/test_calc_on_order_fills_codegen.py | 4 +- tests/test_codegen_new.py | 44 +++++++++++++++++++++- tests/test_collection_scope_isolation.py | 4 +- tests/test_map_call_diagnostics.py | 20 +++++----- tests/test_map_terminal_returns.py | 4 +- tests/test_pinemap_boundaries_and_order.py | 2 +- tests/test_pinemap_semantics.py | 2 +- 11 files changed, 72 insertions(+), 22 deletions(-) diff --git a/pineforge_codegen/codegen/emit_top.py b/pineforge_codegen/codegen/emit_top.py index 707bec5..4c0dcdd 100644 --- a/pineforge_codegen/codegen/emit_top.py +++ b/pineforge_codegen/codegen/emit_top.py @@ -176,6 +176,11 @@ def _emit_includes(self, lines: list[str]) -> None: # byte-identical — mirrors the matrix.hpp gating above. if getattr(self, "_uses_drawing", False): lines.append('#include ') + lines.extend([ + "#ifndef PINEFORGE_HAS_NATIVE_LOWERING_V1", + '#error "generated code requires pineforge-engine native lowering v1 (PINEFORGE_HAS_NATIVE_LOWERING_V1)"', + "#endif", + ]) lines.append("") # Compatibility shim for the namespace-wrap refactor: unqualified # references to BacktestEngine / Bar / na() / ta::* / etc. resolve diff --git a/pineforge_codegen/codegen/visit_call.py b/pineforge_codegen/codegen/visit_call.py index 34e0063..75db91e 100644 --- a/pineforge_codegen/codegen/visit_call.py +++ b/pineforge_codegen/codegen/visit_call.py @@ -2555,8 +2555,8 @@ def _visit_strategy_call(self, func_name: str, node: FuncCall) -> str: return (f"strategy_exit({exit_id}, {from_id}, {limit_val}, {stop_val}, " f"{trail_pts}, {trail_off}, {trail_pr}, {qty_pct}, {comment}, " f"{qty_val}, {oca_val}, {profit_ticks}, {loss_ticks})") - close_comment = self._visit_expr(comment_n) if comment_n is not None else '""' - return f"strategy_close({exit_id}, {close_comment})" + comment = self._visit_expr(comment_n) if comment_n is not None else '""' + return f"strategy_exit_cancel_bracket({exit_id}, {from_id}, {comment})" if func_name == "cancel": p = self._resolve_func_args(node, "strategy.close") # same shape: id first diff --git a/pineforge_codegen/codegen/visit_expr.py b/pineforge_codegen/codegen/visit_expr.py index ea8eff7..1856b99 100644 --- a/pineforge_codegen/codegen/visit_expr.py +++ b/pineforge_codegen/codegen/visit_expr.py @@ -794,7 +794,7 @@ def _visit_member_access(self, node: MemberAccess) -> str: if node.member == "islast": return "barstate_islast_" if node.member == "isnew": - return "is_first_tick_" + return "is_first_tick()" if node.member == "isconfirmed": return "is_last_tick_" if node.member == "ishistory": diff --git a/tests/golden/matrix_eigen_pca.cpp b/tests/golden/matrix_eigen_pca.cpp index aa52a4c..994f0ee 100644 --- a/tests/golden/matrix_eigen_pca.cpp +++ b/tests/golden/matrix_eigen_pca.cpp @@ -22,6 +22,9 @@ #include #include #include +#ifndef PINEFORGE_HAS_NATIVE_LOWERING_V1 +#error "generated code requires pineforge-engine native lowering v1 (PINEFORGE_HAS_NATIVE_LOWERING_V1)" +#endif using namespace pineforge; diff --git a/tests/test_calc_on_order_fills_codegen.py b/tests/test_calc_on_order_fills_codegen.py index 78dbedb..b60007a 100644 --- a/tests/test_calc_on_order_fills_codegen.py +++ b/tests/test_calc_on_order_fills_codegen.py @@ -176,7 +176,7 @@ def test_post_fill_recalc_updates_current_history_slot_but_barstate_stays_new(): fill recalculation. History advancement therefore has a separate runtime predicate: every rolling member updates its existing current-bar slot when the ordinary-close checkpoint is restored, while ``barstate.isnew`` keeps - lowering to ``is_first_tick_``. + lowering to ``is_first_tick()``. """ cpp = transpile(_HISTORY_ADVANCE_PROBE) on_bar = cpp.split("void on_source_bar(const Bar& bar) override {", 1)[1].split( @@ -229,7 +229,7 @@ def test_post_fill_recalc_updates_current_history_slot_but_barstate_stays_new(): # Mutation guard: coupling barstate.isnew to history advancement would make # it false during historical fill recalcs, contrary to Pine semantics. - assert "if (is_first_tick_) {" in on_bar + assert "if (is_first_tick()) {" in on_bar assert "if (history_advances_new_bar()) {\n strategy_entry" not in on_bar diff --git a/tests/test_codegen_new.py b/tests/test_codegen_new.py index 0a6a911..347e942 100644 --- a/tests/test_codegen_new.py +++ b/tests/test_codegen_new.py @@ -90,6 +90,19 @@ def test_includes_present(): assert '#include ' in cpp +def test_native_lowering_capability_is_required_once_after_engine_includes(): + cpp = _generate('//@version=6\nstrategy("T")\n') + guard = ( + "#ifndef PINEFORGE_HAS_NATIVE_LOWERING_V1\n" + '#error "generated code requires pineforge-engine native lowering v1 ' + '(PINEFORGE_HAS_NATIVE_LOWERING_V1)"\n' + "#endif" + ) + assert cpp.count(guard) == 1 + assert cpp.index('#include ') < cpp.index(guard) + assert cpp.index(guard) < cpp.index("using namespace pineforge;") + + def test_class_structure(): cpp = _generate('//@version=6\nstrategy("T")\n') assert "class GeneratedStrategy : public pineforge::source::PineStrategyHost" in cpp @@ -526,6 +539,35 @@ def test_strategy_exit_forwards_comment_to_runtime(): cpp = _generate(src) assert 'std::string("stop exit")' in cpp assert 'strategy_exit(std::string("X"), std::string("Long")' in cpp + assert "strategy_exit_cancel_bracket(" not in cpp + + +def test_priceless_strategy_exit_cancels_bracket_with_from_entry_and_comment(): + cpp = _generate( + 'strategy.exit(id="cancel", from_entry="Long", comment="remove bracket")' + ) + assert ( + 'strategy_exit_cancel_bracket(std::string("cancel"), ' + 'std::string("Long"), std::string("remove bracket"));' + ) in cpp + assert "strategy_close(" not in cpp + + +def test_priceless_strategy_exit_cancels_bracket_with_default_from_entry_and_comment(): + cpp = _generate('strategy.exit("cancel")') + assert 'strategy_exit_cancel_bracket(std::string("cancel"), "", "");' in cpp + assert "strategy_close(" not in cpp + + +def test_strategy_convert_to_account_and_symbol_remain_identity_lowerings(): + cpp = _generate( + "to_account = strategy.convert_to_account(close)\n" + "to_symbol = strategy.convert_to_symbol(open)" + ) + assert "to_account = (current_bar_.close);" in cpp + assert "to_symbol = (current_bar_.open);" in cpp + assert "strategy_convert_to_account" not in cpp + assert "strategy_convert_to_symbol" not in cpp def test_strategy_position_size(): @@ -1555,7 +1597,7 @@ def test_barstate_tick_members_use_runtime_state(): if barstate.isnew and barstate.isconfirmed strategy.entry("L", strategy.long) """) - assert "is_first_tick_" in cpp + assert "is_first_tick()" in cpp assert "is_last_tick_" in cpp diff --git a/tests/test_collection_scope_isolation.py b/tests/test_collection_scope_isolation.py index e2f7d41..c961295 100644 --- a/tests/test_collection_scope_isolation.py +++ b/tests/test_collection_scope_isolation.py @@ -835,7 +835,7 @@ def test_temporal_outer_alias_avoids_user_name_and_routes_subscript() -> None: def test_unique_local_collection_output_hash_is_stable() -> None: cpp = transpile(_IDENTITY_SOURCE) - assert len(cpp) == 14048 + assert len(cpp) == 14200 assert sha256(cpp.encode()).hexdigest() == ( - "3e811b8da0bfa832577a6e261d9f2ee145c6a88368d964d84b9c94cac6ede616" + "95d691b847e65641f38a89c387d5e6638f1061b0c0486e93d50b31bb22219184" ) diff --git a/tests/test_map_call_diagnostics.py b/tests/test_map_call_diagnostics.py index 1b71979..0e00491 100644 --- a/tests/test_map_call_diagnostics.py +++ b/tests/test_map_call_diagnostics.py @@ -64,7 +64,7 @@ def test_duplicate_keyword_argument_is_a_parser_compile_error(): def test_valid_existing_positional_and_typed_keyword_forms_do_not_drift(): cpp = transpile(_VALID_EXISTING_FORMS) assert sha256(cpp.encode()).hexdigest() == ( - "6d8475b06b3393521b6be96296c9791aee166380117be0347279869305e7fff6" + "29350cf5f3c01e8b3009a2c24c3dde7b0def653b1d7586a249027ff24a8fb0d1" ) @@ -78,7 +78,7 @@ def test_valid_existing_positional_and_typed_keyword_forms_do_not_drift(): map.get("key") observed = probe(map.new()) ''', - "c3e1ad52f99ed628a7692606e9aef13c50b6dc22ffa9714d0bf71ccdc8060434", + "75f3301f5bc255c39e176d13577e9347c000c140ba7aee6e02cedf1f2252f732", ), ( '''//@version=6 @@ -89,7 +89,7 @@ def test_valid_existing_positional_and_typed_keyword_forms_do_not_drift(): map.get("key") observed = probe() ''', - "69048a24494dc0d684aabecfbffc5b86aba80ce3c26cbae6848e4ede5ca5bda6", + "133698bbd81b744c544b444b3aebee239d36bf366694267f770fed5916c41d91", ), ( '''//@version=6 @@ -98,7 +98,7 @@ def test_valid_existing_positional_and_typed_keyword_forms_do_not_drift(): map.put("key", 1) observed = map.get("key") ''', - "2f6db3cfce2f6b908fc6c4485c0187a6418e3599a94be678f4bcb43e1e2b3b90", + "b6cc20d55c958298066f641d8558ea74e58ba8ed73fe2d8b60b3e395134f9777", ), ], ) @@ -141,7 +141,7 @@ def test_security_timeframe_clone_keeps_preceding_map_namespace_source_order(): ) assert sha256(cpp.encode()).hexdigest() == ( - "74f31ea3937aed127eed7641d0d9a9f23c64e5c746e09596a8a284799f9b3686" + "72a6cd9615311ffa3ef636f6a2b51ac4d23b4cee9565e5c4b979bc54d5f1fa79" ) @@ -159,7 +159,7 @@ def test_security_timeframe_clone_keeps_visible_map_receiver_source_order(): cpp = transpile(source, filename="synthetic-lexical-map-source-order.pine") assert sha256(cpp.encode()).hexdigest() == ( - "5a087945ff76c28afd2112c7441c43e34436b9f60febf11f37204547a23438ed" + "9c57c2ef6a85ab578b9261d5dc2052c1aa8ccf5718148fbb03c02038a2068680" ) @@ -204,22 +204,22 @@ def test_security_timeframe_clone_keeps_visible_map_receiver_source_order(): [ ( _LATER_GLOBAL_MAP_SOURCE, - "bc28346812b8fb9427e9394f81fe51ca59565fae6daa505ce09cc55b51a1f042", + "4be34d0a72fc017a45c8af577c61cf00e756f750aae3758d6fbf51c0994c1339", 34.0, ), ( _NESTED_LEXICAL_MAP_ROOT_SOURCE, - "b305db152684f9e7496f051f075880cbeed620775d5fbb63beb7d765f2091d04", + "53fac635abe32e0ced972b59d8dad2cddd36475b9d8499838a24f4da21174f14", 7.0, ), ( _BLOCK_LOCAL_MAP_ISOLATION_SOURCE, - "1211d22c8be706089ca26b687259aad3a5ac693621eacc6a6ae0a5a23b72fd1e", + "e3da0e24d4d4f5044ff442139f974262599bada534cea8a240c2884517d3cc3e", 923.0, ), ( _FOR_BLOCK_LOCAL_MAP_SOURCE, - "aa2d0b0195b24dfac6e0dada1f511e3614d25c65a9f1a51b21766c5d4daa23af", + "f8aec0239b2825e34212cbf90aae87fff051171e0a5b27373569e06448bdb19e", 8.0, ), ], diff --git a/tests/test_map_terminal_returns.py b/tests/test_map_terminal_returns.py index 18a508d..1be31d4 100644 --- a/tests/test_map_terminal_returns.py +++ b/tests/test_map_terminal_returns.py @@ -390,7 +390,7 @@ def test_map_terminal_return_forms_compile(): def test_nonterminal_pinemap_output_hash_is_stable(): cpp = transpile(_NONTERMINAL_SOURCE) assert sha256(cpp.encode()).hexdigest() == ( - "5e3d7b6dc5790842e39bef36eb932d1a6b0596baab068c8ffe2294d277c3a5bf" + "441f37c04025dfc2771de3ae507d1760f9505676bb5055fe2431925bc54d3103" ) @@ -425,5 +425,5 @@ def test_invalid_terminal_map_shapes_raise_compile_errors(): def test_unresolved_parameter_keeps_lexical_precedence_over_global_map(): cpp = transpile(_SHADOWED_UNRESOLVED_PARAM_SOURCE) assert sha256(cpp.encode()).hexdigest() == ( - "135b9183598b99e6461d1f58b9fa7965c7283abcc21b83ea226e50373aa90885" + "95b2b176ea93b27c467b805193c2d2098d70988e420aaf75b2f22bcc05760769" ) diff --git a/tests/test_pinemap_boundaries_and_order.py b/tests/test_pinemap_boundaries_and_order.py index 0a5a389..ddf029c 100644 --- a/tests/test_pinemap_boundaries_and_order.py +++ b/tests/test_pinemap_boundaries_and_order.py @@ -208,7 +208,7 @@ def test_non_map_user_call_remains_exact_baseline_bytes() -> None: cpp = transpile(source) assert "__pf_call_arg_" not in cpp assert sha256(cpp.encode()).hexdigest() == ( - "f448daf0f4ded4554d117c74414ea6325e189ff70b3c7f1471c3072a7477c997" + "a876072a5b2827875f3ff271690d22213cd859681bdd832f7e8edc0e3f13ad01" ) diff --git a/tests/test_pinemap_semantics.py b/tests/test_pinemap_semantics.py index ae738db..9aa575f 100644 --- a/tests/test_pinemap_semantics.py +++ b/tests/test_pinemap_semantics.py @@ -388,7 +388,7 @@ def test_non_map_cpp_remains_exact_baseline_bytes() -> None: # Whole-output pin includes the generated source-host constructor and # lifecycle reset. The ordinary non-map lowering remains unchanged. assert sha256(cpp.encode()).hexdigest() == ( - "1d822b51179dfff05d5b1ecc9b2bfdb7ae2e33b2d497463b96c9f4d6b9b30a38" + "5979a0d3a4465192b456635c5dfb590358c353d547ec80de368e54629b807afb" ) assert '#include ' not in cpp assert "_PFCheckpointTraits" not in cpp