Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pineforge_codegen/codegen/emit_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <pineforge/drawing.hpp>')
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<T>() / ta::* / etc. resolve
Expand Down
4 changes: 2 additions & 2 deletions pineforge_codegen/codegen/visit_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pineforge_codegen/codegen/visit_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
3 changes: 3 additions & 0 deletions tests/golden/matrix_eigen_pca.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
#include <pineforge/str_utils.hpp>
#include <pineforge/session_time.hpp>
#include <pineforge/matrix.hpp>
#ifndef PINEFORGE_HAS_NATIVE_LOWERING_V1
#error "generated code requires pineforge-engine native lowering v1 (PINEFORGE_HAS_NATIVE_LOWERING_V1)"
#endif

using namespace pineforge;

Expand Down
4 changes: 2 additions & 2 deletions tests/test_calc_on_order_fills_codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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


Expand Down
44 changes: 43 additions & 1 deletion tests/test_codegen_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ def test_includes_present():
assert '#include <pineforge/ta.hpp>' 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 <pineforge/source/pine_strategy_host.hpp>') < 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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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


Expand Down
4 changes: 2 additions & 2 deletions tests/test_collection_scope_isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
20 changes: 10 additions & 10 deletions tests/test_map_call_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)


Expand All @@ -78,7 +78,7 @@ def test_valid_existing_positional_and_typed_keyword_forms_do_not_drift():
map.get("key")
observed = probe(map.new<string, int>())
''',
"c3e1ad52f99ed628a7692606e9aef13c50b6dc22ffa9714d0bf71ccdc8060434",
"75f3301f5bc255c39e176d13577e9347c000c140ba7aee6e02cedf1f2252f732",
),
(
'''//@version=6
Expand All @@ -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
Expand All @@ -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",
),
],
)
Expand Down Expand Up @@ -141,7 +141,7 @@ def test_security_timeframe_clone_keeps_preceding_map_namespace_source_order():
)

assert sha256(cpp.encode()).hexdigest() == (
"74f31ea3937aed127eed7641d0d9a9f23c64e5c746e09596a8a284799f9b3686"
"72a6cd9615311ffa3ef636f6a2b51ac4d23b4cee9565e5c4b979bc54d5f1fa79"
)


Expand All @@ -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"
)


Expand Down Expand Up @@ -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,
),
],
Expand Down
4 changes: 2 additions & 2 deletions tests/test_map_terminal_returns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)


Expand Down Expand Up @@ -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"
)
2 changes: 1 addition & 1 deletion tests/test_pinemap_boundaries_and_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)


Expand Down
2 changes: 1 addition & 1 deletion tests/test_pinemap_semantics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <pineforge/map.hpp>' not in cpp
assert "_PFCheckpointTraits" not in cpp
Expand Down
Loading