opencode-pine2pyne 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/PKG-INFO +1 -1
  2. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/PKG-INFO +1 -1
  3. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/SOURCES.txt +6 -0
  4. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/__init__.py +1 -1
  5. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/ast_nodes.py +1 -0
  6. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/codegen.py +3 -2
  7. opencode_pine2pyne-0.2.1/pine2pyne/conditional_history_calls.py +96 -0
  8. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/errors.py +2 -1
  9. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/import_resolver.py +4 -1
  10. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/parser.py +107 -29
  11. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/pine_builtins.py +31 -2
  12. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/pine_semantics.py +18 -11
  13. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/scope_renamer.py +12 -6
  14. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/transformer.py +23 -21
  15. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pyproject.toml +3 -0
  16. opencode_pine2pyne-0.2.1/tests/test_assignment_to_a_call_result_field.py +148 -0
  17. opencode_pine2pyne-0.2.1/tests/test_builtin_named_function_and_enum_input.py +122 -0
  18. opencode_pine2pyne-0.2.1/tests/test_conditional_history_call_warning.py +127 -0
  19. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_errors.py +3 -0
  20. opencode_pine2pyne-0.2.1/tests/test_input_numeric_positional_binding.py +97 -0
  21. opencode_pine2pyne-0.2.1/tests/test_loop_and_array_edge_cases.py +97 -0
  22. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/LICENSE +0 -0
  23. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/README.md +0 -0
  24. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/dependency_links.txt +0 -0
  25. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/entry_points.txt +0 -0
  26. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/requires.txt +0 -0
  27. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/top_level.txt +0 -0
  28. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/README.md +0 -0
  29. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/TRANSPILER_BEST_PRACTICES.md +0 -0
  30. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/TRANSPILER_USAGE.md +0 -0
  31. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/__main__.py +0 -0
  32. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/ast_walk.py +0 -0
  33. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/block_value_lowering.py +0 -0
  34. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/bool_builtins.py +0 -0
  35. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/bool_value_typing.py +0 -0
  36. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/cli.py +0 -0
  37. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/emitted_names.py +0 -0
  38. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/for_loop_end_bound.py +0 -0
  39. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/grouped_history_lowering.py +0 -0
  40. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/lexer.py +0 -0
  41. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/library_collision.py +0 -0
  42. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/namespace_members.py +0 -0
  43. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/once_lowering.py +0 -0
  44. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/overload_resolution.py +0 -0
  45. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/parse_repair.py +0 -0
  46. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/strict_boolean.py +0 -0
  47. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/symbol_table.py +0 -0
  48. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/tokens.py +0 -0
  49. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/type_inference.py +0 -0
  50. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/udt_field_defaults.py +0 -0
  51. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/setup.cfg +0 -0
  52. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_block_tail_value_runtime.py +0 -0
  53. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_block_value_runtime.py +0 -0
  54. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_cli.py +0 -0
  55. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_codegen.py +0 -0
  56. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_codegen_operator_grouping.py +0 -0
  57. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_codegen_refusal.py +0 -0
  58. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_collect_namespace_members.py +0 -0
  59. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_compare_ground_truth_completeness.py +0 -0
  60. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_corpus_regression.py +0 -0
  61. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_dotted_udt_default_runtime.py +0 -0
  62. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_drawing_receiver_runtime.py +0 -0
  63. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_emitted_name_collision_runtime.py +0 -0
  64. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_emitted_name_plan.py +0 -0
  65. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_error_demo_claims.py +0 -0
  66. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_error_demo_runtime.py +0 -0
  67. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_for_in_pairs_runtime.py +0 -0
  68. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_for_loop_dynamic_end_runtime.py +0 -0
  69. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_function_tail_runtime.py +0 -0
  70. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_generated_tables.py +0 -0
  71. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_check_no_output.py +0 -0
  72. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_check_own_directory.py +0 -0
  73. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_digests.py +0 -0
  74. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_manifest_order.py +0 -0
  75. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_no_output.py +0 -0
  76. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_run_dir_isolation.py +0 -0
  77. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_settings.py +0 -0
  78. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_wrapper_retired.py +0 -0
  79. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_grouped_history_lowering.py +0 -0
  80. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_grouped_history_runtime.py +0 -0
  81. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_import_resolver.py +0 -0
  82. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_input_constant_and_enum_order_runtime.py +0 -0
  83. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_input_history_read_runtime.py +0 -0
  84. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_lexer.py +0 -0
  85. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_library_name_collision.py +0 -0
  86. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_library_name_collision_runtime.py +0 -0
  87. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_method_namespaces.py +0 -0
  88. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_method_receiver_shape_runtime.py +0 -0
  89. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_method_routing_runtime.py +0 -0
  90. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_once_structure_parser.py +0 -0
  91. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_once_structure_runtime.py +0 -0
  92. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_overload_dispatch_runtime.py +0 -0
  93. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_overload_resolution.py +0 -0
  94. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_parse_repair_card55.py +0 -0
  95. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_parser.py +0 -0
  96. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_parser_positions.py +0 -0
  97. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_pine_builtins.py +0 -0
  98. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_pine_semantics.py +0 -0
  99. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_release_artifact_grouping.py +0 -0
  100. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_runtime_floor.py +0 -0
  101. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_scope_model_drift.py +0 -0
  102. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_scope_renamer.py +0 -0
  103. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_screener_regression.py +0 -0
  104. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_script_declaration_binding.py +0 -0
  105. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_shadowing_runtime.py +0 -0
  106. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_strict_and_or_runtime.py +0 -0
  107. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_suite_harness_exit_verdict.py +0 -0
  108. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_suite_report_compare.py +0 -0
  109. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_switch_subject_runtime.py +0 -0
  110. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_symbol_table.py +0 -0
  111. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ta_conditional_runtime.py +0 -0
  112. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_transformer.py +0 -0
  113. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_transformer_receiver_typing.py +0 -0
  114. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_transformer_single_use.py +0 -0
  115. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_udt_array_new_runtime.py +0 -0
  116. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_udt_bool_field_default_by_version_runtime.py +0 -0
  117. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_udt_field_defaults_runtime.py +0 -0
  118. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_udt_field_history_runtime.py +0 -0
  119. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_udt_type_proof.py +0 -0
  120. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_untyped_na_runtime.py +0 -0
  121. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_user_declaration_shadows_builtin_runtime.py +0 -0
  122. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_v5_bool_cast_runtime.py +0 -0
  123. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_var_history_runtime.py +0 -0
  124. {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_variable_function_runtime.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-pine2pyne
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Pine Script v6 to PyneCore Python transpiler
5
5
  Author: rubycell
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-pine2pyne
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Pine Script v6 to PyneCore Python transpiler
5
5
  Author: rubycell
6
6
  License-Expression: Apache-2.0
@@ -19,6 +19,7 @@ pine2pyne/bool_builtins.py
19
19
  pine2pyne/bool_value_typing.py
20
20
  pine2pyne/cli.py
21
21
  pine2pyne/codegen.py
22
+ pine2pyne/conditional_history_calls.py
22
23
  pine2pyne/emitted_names.py
23
24
  pine2pyne/errors.py
24
25
  pine2pyne/for_loop_end_bound.py
@@ -40,14 +41,17 @@ pine2pyne/tokens.py
40
41
  pine2pyne/transformer.py
41
42
  pine2pyne/type_inference.py
42
43
  pine2pyne/udt_field_defaults.py
44
+ tests/test_assignment_to_a_call_result_field.py
43
45
  tests/test_block_tail_value_runtime.py
44
46
  tests/test_block_value_runtime.py
47
+ tests/test_builtin_named_function_and_enum_input.py
45
48
  tests/test_cli.py
46
49
  tests/test_codegen.py
47
50
  tests/test_codegen_operator_grouping.py
48
51
  tests/test_codegen_refusal.py
49
52
  tests/test_collect_namespace_members.py
50
53
  tests/test_compare_ground_truth_completeness.py
54
+ tests/test_conditional_history_call_warning.py
51
55
  tests/test_corpus_regression.py
52
56
  tests/test_dotted_udt_default_runtime.py
53
57
  tests/test_drawing_receiver_runtime.py
@@ -73,9 +77,11 @@ tests/test_grouped_history_runtime.py
73
77
  tests/test_import_resolver.py
74
78
  tests/test_input_constant_and_enum_order_runtime.py
75
79
  tests/test_input_history_read_runtime.py
80
+ tests/test_input_numeric_positional_binding.py
76
81
  tests/test_lexer.py
77
82
  tests/test_library_name_collision.py
78
83
  tests/test_library_name_collision_runtime.py
84
+ tests/test_loop_and_array_edge_cases.py
79
85
  tests/test_method_namespaces.py
80
86
  tests/test_method_receiver_shape_runtime.py
81
87
  tests/test_method_routing_runtime.py
@@ -5,7 +5,7 @@ A tool to convert TradingView Pine Script indicators and strategies
5
5
  to PyneCore Python code for local backtesting and analysis.
6
6
  """
7
7
 
8
- __version__ = '0.2.0'
8
+ __version__ = '0.2.1'
9
9
  __author__ = 'Pine2Pyne Transpiler'
10
10
 
11
11
  from .lexer import Lexer, tokenize
@@ -302,6 +302,7 @@ class ForLoop(Statement):
302
302
  # counter (`pine_loop`) that re-reads it; empty for a fixed `pine_range` (card #82)
303
303
  counter_name: str = ""
304
304
  counter_helper: str = "" # the name `pine_loop` is imported under (an alias when the script uses the name)
305
+ range_helper: str = "" # the name `pine_range` is imported under for a fixed loop; empty is `pine_range` (card #94)
305
306
 
306
307
 
307
308
  @dataclass
@@ -542,11 +542,12 @@ class CodeGenerator:
542
542
  self.generate_statement(s)
543
543
  self.indent_level -= 1
544
544
  return
545
+ range_helper = stmt.range_helper or 'pine_range'
545
546
  if stmt.step:
546
547
  step = self.generate_expression(stmt.step)
547
- self.emit(f'for {var} in pine_range({from_val}, {to_val}, {step}):')
548
+ self.emit(f'for {var} in {range_helper}({from_val}, {to_val}, {step}):')
548
549
  else:
549
- self.emit(f'for {var} in pine_range({from_val}, {to_val}):')
550
+ self.emit(f'for {var} in {range_helper}({from_val}, {to_val}):')
550
551
 
551
552
  self.indent_level += 1
552
553
  for s in stmt.body:
@@ -0,0 +1,96 @@
1
+ """A `ta.*` call that runs on only some bars (card #96).
2
+
3
+ Pine keeps a call site's series history by running the call on every bar. A `ta.*` call
4
+ reached only when a condition holds keeps a history that skips the other bars, so its value
5
+ differs from the same call made on every bar (card #60 measured this: the transpiled output
6
+ matches Pine's call-site semantics in every construct, and a hoisted call gives another value).
7
+ The Pine User Manual (Execution model) says to run such functions in the global scope for
8
+ consistent results. The transpiler never hoists: it keeps the call where the script has it and
9
+ reports the site, as TradingView does, so the author decides.
10
+
11
+ A call is conditional when it sits in:
12
+ - the body of an `if`, `else if` or `else`, or an `else if` condition (it is read only when the
13
+ earlier ones failed), as a statement or as a value;
14
+ - a branch of `?:`;
15
+ - a `switch` arm or its default, or a case after the first (the first case is always read);
16
+ - the right operand of `and` / `or` in Pine v6, which evaluates them lazily. Pine v5 and earlier
17
+ evaluate both operands (card #65), so the right operand there is not conditional.
18
+
19
+ A function body starts unconditional again: whether the function is called conditionally is not
20
+ decided here. A loop body is not conditional. `ta.vwap` and the other argument-free `ta.`
21
+ variables are not calls and are hoisted by the transpiler already.
22
+ """
23
+ from typing import List
24
+
25
+ from .ast_nodes import (
26
+ BinaryOp, FuncDecl, FunctionCall, IfExpression, IfStatement, Script, SwitchExpression, SwitchStatement,
27
+ TernaryOp,
28
+ )
29
+ from .ast_walk import child_field_names, is_node
30
+ from .strict_boolean import BOOLEAN_OPERATORS, evaluates_and_or_strictly
31
+
32
+ HISTORY_CALL_PREFIX = 'ta.'
33
+
34
+
35
+ def conditional_history_calls(script: Script) -> List[FunctionCall]:
36
+ """Every `ta.*` call of `script` that runs on only some bars, in source order."""
37
+ finder = _ConditionalCallFinder(evaluates_and_or_strictly(script))
38
+ finder.visit(script, False)
39
+ return sorted(finder.calls, key=lambda call: (call.line, call.column))
40
+
41
+
42
+ class _ConditionalCallFinder:
43
+ def __init__(self, right_operand_always_runs: bool):
44
+ self.right_operand_always_runs = right_operand_always_runs
45
+ self.calls: List[FunctionCall] = []
46
+
47
+ def visit(self, value, conditional: bool) -> None:
48
+ if isinstance(value, (list, tuple)):
49
+ for item in value:
50
+ self.visit(item, conditional)
51
+ elif isinstance(value, dict):
52
+ for item in value.values():
53
+ self.visit(item, conditional)
54
+ elif is_node(value):
55
+ self.visit_node(value, conditional)
56
+
57
+ def visit_node(self, node, conditional: bool) -> None:
58
+ if isinstance(node, (IfStatement, IfExpression, TernaryOp)):
59
+ self.visit_branches(node, conditional)
60
+ elif isinstance(node, (SwitchStatement, SwitchExpression)):
61
+ self.visit_switch(node, conditional)
62
+ elif isinstance(node, BinaryOp) and node.op in BOOLEAN_OPERATORS:
63
+ self.visit(node.left, conditional)
64
+ self.visit(node.right, conditional or not self.right_operand_always_runs)
65
+ elif isinstance(node, FuncDecl):
66
+ self.visit(node.params, False)
67
+ self.visit(node.body, False)
68
+ else:
69
+ if conditional and isinstance(node, FunctionCall) and _is_history_call(node):
70
+ self.calls.append(node)
71
+ for name in child_field_names(type(node)):
72
+ self.visit(getattr(node, name), conditional)
73
+
74
+ def visit_branches(self, node, conditional: bool) -> None:
75
+ """The condition always runs; each branch and every `else if` condition only after it."""
76
+ self.visit(node.condition, conditional)
77
+ if isinstance(node, IfStatement):
78
+ self.visit(node.body, True)
79
+ for condition, body in node.elseifs:
80
+ self.visit(condition, True)
81
+ self.visit(body, True)
82
+ self.visit(node.else_body, True)
83
+ else:
84
+ self.visit(node.true_expr, True)
85
+ self.visit(node.false_expr, True)
86
+
87
+ def visit_switch(self, node, conditional: bool) -> None:
88
+ self.visit(node.expr, conditional)
89
+ for index, (case, body) in enumerate(node.cases):
90
+ self.visit(case, conditional if index == 0 else True)
91
+ self.visit(body, True)
92
+ self.visit(node.default, True)
93
+
94
+
95
+ def _is_history_call(call: FunctionCall) -> bool:
96
+ return isinstance(call.func, str) and call.func.startswith(HISTORY_CALL_PREFIX)
@@ -53,7 +53,8 @@ class SemanticError(Pine2PyneError):
53
53
  class SemanticWarning(UserWarning):
54
54
  """A program TradingView refuses that is still transpiled, output unchanged
55
55
  (operator decision 2026-09-24, card #28): a same-scope redeclaration, a read
56
- before the declaration. Carries the rule's `code` and the Pine position;
56
+ before the declaration; and, though TradingView compiles it, a `ta.*` call
57
+ that runs on only some bars (card #96). Carries the rule's `code` and the Pine position;
57
58
  under `-W error` it propagates as an exception like a refusal."""
58
59
  def __init__(self, code: str, message: str, line: int = 0, column: int = 0):
59
60
  super().__init__(f'{line}:{column}: {message} [{code}]')
@@ -31,6 +31,7 @@ class ImportResolver:
31
31
  self.uses_method_call = False # method_call for explicit method calling
32
32
  self.uses_overload = False # pynecore @overload for same-name declarations
33
33
  self.uses_pine_range = False # pine_range for loops
34
+ self.pine_range_name = 'pine_range' # the name pine_range is imported under; the transformer sets an alias when the script uses the name (card #94)
34
35
  self.pine_loop_name = '' # the name pine_loop is imported under: a v6 for loop re-reads its end (card #82)
35
36
  self.uses_cast_int = False # cast_int for type casting
36
37
  self.uses_cast_float = False # cast_float for type casting
@@ -308,8 +309,10 @@ class ImportResolver:
308
309
  imports.append("from pynecore import pine_loop")
309
310
  elif self.pine_loop_name:
310
311
  imports.append(f"from pynecore import pine_loop as {self.pine_loop_name}")
311
- if self.uses_pine_range:
312
+ if self.uses_pine_range and self.pine_range_name == 'pine_range':
312
313
  imports.append("from pynecore import pine_range")
314
+ elif self.uses_pine_range:
315
+ imports.append(f"from pynecore import pine_range as {self.pine_range_name}")
313
316
 
314
317
  # 2. Pine cast (if used)
315
318
  if self.uses_cast_int or self.uses_cast_float or self.uses_cast_bool:
@@ -8,8 +8,9 @@ import warnings
8
8
  from typing import List, Optional, Union
9
9
  from .tokens import Token, TokenType
10
10
  from .ast_nodes import *
11
- from .errors import ParserError, SemanticError
11
+ from .errors import ParserError, SemanticError, UnsupportedFeatureError
12
12
  from .once_lowering import lower_once_structures
13
+ from .scope_renamer import fresh_name
13
14
 
14
15
 
15
16
  def _pine_version(version: Optional[VersionAnnotation]) -> int:
@@ -18,6 +19,26 @@ def _pine_version(version: Optional[VersionAnnotation]) -> int:
18
19
  return int(text) if str(text).isdigit() else 0
19
20
 
20
21
 
22
+ # `x += 5` is read as `x := x + 5`: the operator each compound assignment token stands for
23
+ COMPOUND_ASSIGNMENT_OPERATORS = {
24
+ TokenType.PLUS_ASSIGN: '+',
25
+ TokenType.MINUS_ASSIGN: '-',
26
+ TokenType.MULT_ASSIGN: '*',
27
+ TokenType.DIV_ASSIGN: '/',
28
+ TokenType.MOD_ASSIGN: '%',
29
+ }
30
+ # The variable that holds the receiver of `arr.get(0).v += 1` (card #98)
31
+ RECEIVER_NAME_STEM = 'assigned_field_receiver'
32
+
33
+
34
+ def _as_statement_list(statement) -> list:
35
+ """What parse_statement returned as a list: one statement, several (a source statement
36
+ lowered to more than one, card #98), or none."""
37
+ if isinstance(statement, list):
38
+ return statement
39
+ return [statement] if statement else []
40
+
41
+
21
42
  class Parser:
22
43
  """Recursive descent parser for Pine Script v6."""
23
44
 
@@ -29,6 +50,7 @@ class Parser:
29
50
  # instead; the meaningful VERSION_ANNOTATION is preserved.
30
51
  self.tokens = [t for t in tokens if t.type is not TokenType.COMPILER_ANNOTATION]
31
52
  self.pos = 0
53
+ self._token_names: Optional[set] = None # every name the tokens hold, read on first use (card #98)
32
54
 
33
55
  def current_token(self) -> Token:
34
56
  """Get current token without advancing."""
@@ -221,14 +243,12 @@ class Parser:
221
243
  # Not a simple input declaration (e.g., input.string(...) == "On")
222
244
  # Fall back to parsing as a regular statement
223
245
  self.pos = saved_pos
224
- stmt = self.parse_statement()
225
- if stmt:
246
+ for stmt in _as_statement_list(self.parse_statement()):
226
247
  self._stamp_top_level(stmt, start_token)
227
248
  script.body.append(stmt)
228
249
  # Otherwise, parse as statement
229
250
  else:
230
- stmt = self.parse_statement()
231
- if stmt:
251
+ for stmt in _as_statement_list(self.parse_statement()):
232
252
  self._stamp_top_level(stmt, start_token)
233
253
  script.body.append(stmt)
234
254
 
@@ -803,8 +823,9 @@ class Parser:
803
823
  # Statement parsing
804
824
  # ========================================================================
805
825
 
806
- def parse_statement(self) -> Optional[Statement]:
807
- """Parse a single statement."""
826
+ def parse_statement(self) -> Union[Statement, List[Statement], None]:
827
+ """Parse one source statement: a statement, a list of statements when the source statement is
828
+ lowered to several (an assignment to a field of a call result, card #98), or None."""
808
829
  self.skip_newlines()
809
830
 
810
831
  # If statement
@@ -903,13 +924,20 @@ class Parser:
903
924
  return self.parse_function_declaration()
904
925
 
905
926
  # Assignment or expression statement
927
+ start_token = self.current_token()
906
928
  expr = self.parse_expression()
907
929
 
908
930
  # Check for assignment/reassignment
909
931
  if self.match(TokenType.ASSIGN):
910
- self._refuse_history_target(expr, self.advance())
932
+ operator_token = self.advance()
933
+ self._refuse_history_target(expr, operator_token)
934
+ call_result_field = self._is_call_result_field(expr)
935
+ if not call_result_field:
936
+ self._refuse_unassignable_target(expr, operator_token, tuple_allowed=True)
911
937
  value = self.parse_expression()
912
938
 
939
+ if call_result_field:
940
+ return self._lower_call_result_field_assignment(expr, operator_token, value, start_token)
913
941
  if isinstance(expr, Identifier):
914
942
  return Assignment(target=expr.name, value=value)
915
943
  elif isinstance(expr, ArrayLiteral):
@@ -918,9 +946,15 @@ class Parser:
918
946
  return Assignment(target=TupleDestructure(names=names, value=value), value=value)
919
947
 
920
948
  elif self.match(TokenType.REASSIGN):
921
- self._refuse_history_target(expr, self.advance())
949
+ operator_token = self.advance()
950
+ self._refuse_history_target(expr, operator_token)
951
+ call_result_field = self._is_call_result_field(expr)
952
+ if not call_result_field:
953
+ self._refuse_unassignable_target(expr, operator_token)
922
954
  value = self.parse_expression()
923
955
 
956
+ if call_result_field:
957
+ return self._lower_call_result_field_assignment(expr, operator_token, value, start_token)
924
958
  if isinstance(expr, Identifier):
925
959
  return Reassignment(target=expr.name, value=value)
926
960
 
@@ -929,19 +963,16 @@ class Parser:
929
963
  TokenType.DIV_ASSIGN, TokenType.MOD_ASSIGN):
930
964
  op_token = self.advance()
931
965
  self._refuse_history_target(expr, op_token)
966
+ call_result_field = self._is_call_result_field(expr)
967
+ if not call_result_field:
968
+ self._refuse_unassignable_target(expr, op_token)
932
969
  right_value = self.parse_expression()
933
970
 
971
+ if call_result_field:
972
+ return self._lower_call_result_field_assignment(expr, op_token, right_value, start_token)
934
973
  if isinstance(expr, Identifier):
935
974
  # Convert x += 5 to x := x + 5
936
- # Determine the operator symbol
937
- op_map = {
938
- TokenType.PLUS_ASSIGN: '+',
939
- TokenType.MINUS_ASSIGN: '-',
940
- TokenType.MULT_ASSIGN: '*',
941
- TokenType.DIV_ASSIGN: '/',
942
- TokenType.MOD_ASSIGN: '%',
943
- }
944
- op_symbol = op_map[op_token.type]
975
+ op_symbol = COMPOUND_ASSIGNMENT_OPERATORS[op_token.type]
945
976
 
946
977
  # Create binary expression: x + 5
947
978
  from .ast_nodes import BinaryOp
@@ -964,6 +995,56 @@ class Parser:
964
995
  f'cannot assign with {operator_token.value!r} to a history reference (x[n])',
965
996
  operator_token.line, operator_token.column)
966
997
 
998
+ @staticmethod
999
+ def _is_call_result_field(target: Expression) -> bool:
1000
+ """`arr.get(0).v`: a field of the value an expression returns (card #98)."""
1001
+ return isinstance(target, MemberAccess) and not isinstance(target.object, str)
1002
+
1003
+ def _lower_call_result_field_assignment(self, target: MemberAccess, operator_token: Token, value: Expression,
1004
+ start_token: Token) -> List[Statement]:
1005
+ """`arr.get(0).v += 1` -> `assigned_field_receiver1 = arr.get(0)` then
1006
+ `assigned_field_receiver1.v := assigned_field_receiver1.v + 1` (card #98).
1007
+
1008
+ The receiver is the object a call returns, so the field changes in that object; binding it
1009
+ to a variable first evaluates the call once, as Pine does, and uses the dotted-name
1010
+ assignment every later stage already handles. The variable is fresh against every
1011
+ identifier of the script."""
1012
+ receiver = self._fresh_receiver_name()
1013
+ field = f'{receiver}.{target.member}'
1014
+ position = self._position_of(start_token)
1015
+ if operator_token.type is TokenType.ASSIGN:
1016
+ assigned = Assignment(target=field, value=value)
1017
+ elif operator_token.type is TokenType.REASSIGN:
1018
+ assigned = Reassignment(target=field, value=value)
1019
+ else:
1020
+ current = Identifier(name=field, **position)
1021
+ assigned = Reassignment(target=field, value=BinaryOp(
1022
+ left=current, op=COMPOUND_ASSIGNMENT_OPERATORS[operator_token.type], right=value, **position))
1023
+ statements = [Assignment(target=receiver, value=target.object), assigned]
1024
+ for statement in statements:
1025
+ statement.line, statement.column = position.get('line', 0), position.get('column', 0)
1026
+ return statements
1027
+
1028
+ def _fresh_receiver_name(self) -> str:
1029
+ """`assigned_field_receiver<N>`, the lowest N no identifier of the script holds."""
1030
+ if self._token_names is None:
1031
+ self._token_names = {part for token in self.tokens if isinstance(token.value, str)
1032
+ for part in token.value.split('.')}
1033
+ return fresh_name(RECEIVER_NAME_STEM, self._token_names)
1034
+
1035
+ @staticmethod
1036
+ def _refuse_unassignable_target(target: Expression, operator_token: Token, tuple_allowed: bool = False) -> None:
1037
+ """`arr.get(0).v += 1` is valid Pine, but the tree keeps an assignment only to a name (a
1038
+ dotted field path included) or, for `=`, a tuple. Any other target used to be dropped
1039
+ without a word, leaving a bare expression statement, so it is refused at the operator
1040
+ (card #94)."""
1041
+ if isinstance(target, Identifier) or (tuple_allowed and isinstance(target, ArrayLiteral)):
1042
+ return
1043
+ raise UnsupportedFeatureError(
1044
+ f'assignment with {operator_token.value!r} to the result of an expression; '
1045
+ f'assign the receiver to a variable first and assign its field',
1046
+ operator_token.line, operator_token.column)
1047
+
967
1048
  def parse_block(self) -> List[Statement]:
968
1049
  """Parse a block of statements (indented)."""
969
1050
  statements = []
@@ -973,17 +1054,13 @@ class Parser:
973
1054
  if self.match(TokenType.DEDENT, TokenType.EOF):
974
1055
  break
975
1056
 
976
- stmt = self._parse_positioned_statement()
977
- if stmt:
978
- statements.append(stmt)
1057
+ statements.extend(_as_statement_list(self._parse_positioned_statement()))
979
1058
 
980
1059
  # Handle comma-separated statements on the same line
981
1060
  # e.g., colNum = 8, rowNum = 8
982
1061
  while self.match(TokenType.COMMA):
983
1062
  self.advance() # consume comma
984
- stmt = self._parse_positioned_statement()
985
- if stmt:
986
- statements.append(stmt)
1063
+ statements.extend(_as_statement_list(self._parse_positioned_statement()))
987
1064
 
988
1065
  return statements
989
1066
 
@@ -1011,7 +1088,10 @@ class Parser:
1011
1088
 
1012
1089
  def _parse_positioned_statement(self):
1013
1090
  start_token = self.current_token()
1014
- return self._stamp_nested(self.parse_statement(), start_token)
1091
+ statement = self.parse_statement()
1092
+ if isinstance(statement, list): # one source statement lowered to several (card #98)
1093
+ return [self._stamp_nested(item, start_token) for item in statement]
1094
+ return self._stamp_nested(statement, start_token)
1015
1095
 
1016
1096
  def _parse_positioned_single_line_item(self):
1017
1097
  start_token = self.current_token()
@@ -1207,8 +1287,7 @@ class Parser:
1207
1287
  default = self.parse_block()
1208
1288
  self.expect(TokenType.DEDENT)
1209
1289
  else:
1210
- case_stmt = self._parse_positioned_statement()
1211
- default = [case_stmt] if case_stmt else []
1290
+ default = _as_statement_list(self._parse_positioned_statement())
1212
1291
  self.skip_newlines()
1213
1292
  continue
1214
1293
 
@@ -1225,8 +1304,7 @@ class Parser:
1225
1304
  self.expect(TokenType.DEDENT)
1226
1305
  else:
1227
1306
  # Single line
1228
- case_stmt = self._parse_positioned_statement()
1229
- case_body = [case_stmt] if case_stmt else []
1307
+ case_body = _as_statement_list(self._parse_positioned_statement())
1230
1308
 
1231
1309
  cases.append((case_expr, case_body))
1232
1310
 
@@ -233,6 +233,7 @@ INPUT_FUNCTIONS: Set[str] = {
233
233
  'input.symbol',
234
234
  'input.price',
235
235
  'input.time',
236
+ 'input.enum',
236
237
  }
237
238
 
238
239
 
@@ -244,8 +245,8 @@ def is_input_function(func_name: str) -> bool:
244
245
  # Pine Script input.* positional parameter names (after defval, title).
245
246
  # Pine Script allows these as positional; PyneCore requires keyword-only.
246
247
  INPUT_POSITIONAL_PARAMS: Dict[str, list[str]] = {
247
- 'input.int': ['minval', 'maxval', 'step', 'tooltip', 'inline', 'group', 'confirm', 'display', 'options'],
248
- 'input.float': ['minval', 'maxval', 'step', 'tooltip', 'inline', 'group', 'confirm', 'display', 'options'],
248
+ 'input.int': ['minval', 'maxval', 'step', 'tooltip', 'inline', 'group', 'confirm', 'display', 'active'],
249
+ 'input.float': ['minval', 'maxval', 'step', 'tooltip', 'inline', 'group', 'confirm', 'display', 'active'],
249
250
  'input.bool': ['tooltip', 'inline', 'group', 'confirm', 'display'],
250
251
  'input.string': ['options', 'tooltip', 'inline', 'group', 'confirm', 'display'],
251
252
  'input.color': ['tooltip', 'inline', 'group', 'confirm', 'display'],
@@ -256,8 +257,36 @@ INPUT_POSITIONAL_PARAMS: Dict[str, list[str]] = {
256
257
  'input.source': ['tooltip', 'inline', 'group', 'display', 'active', 'confirm'],
257
258
  'input.price': ['tooltip', 'inline', 'group', 'confirm', 'display'],
258
259
  'input.time': ['tooltip', 'inline', 'group', 'confirm', 'display'],
260
+ 'input.enum': ['options', 'tooltip', 'inline', 'group', 'confirm', 'display', 'active'], # card #97
259
261
  }
260
262
 
263
+ # input.int/input.float have a second positional form after `title` (v6 reference): the options form,
264
+ # selected by a list third argument (pynecore's _bind_numeric_tail, _NUMERIC_OPTIONS_TAIL; card #95).
265
+ NUMERIC_INPUT_FUNCTIONS: FrozenSet[str] = frozenset({'input.int', 'input.float'})
266
+ NUMERIC_INPUT_OPTIONS_POSITIONAL_PARAMS: list[str] = ['options', 'tooltip', 'inline', 'group', 'confirm',
267
+ 'display', 'active']
268
+
269
+
270
+ def input_positional_param_names(func_name: str, positional_args: list) -> list[str]:
271
+ """The names Pine gives the positional arguments of an input.* call after `defval` and `title`
272
+ (empty for a function this table does not list). The one place that decides which positional
273
+ form a call uses, for the transformer and the semantic pass alike."""
274
+ from .ast_nodes import ArrayLiteral
275
+ if func_name in NUMERIC_INPUT_FUNCTIONS and len(positional_args) > 2 \
276
+ and isinstance(positional_args[2], ArrayLiteral):
277
+ return NUMERIC_INPUT_OPTIONS_POSITIONAL_PARAMS
278
+ return INPUT_POSITIONAL_PARAMS.get(func_name, [])
279
+
280
+
281
+ def bind_input_positional_args(func_name: str, positional_args: list) -> Tuple[list, dict]:
282
+ """Split an input.* call's positional arguments into `defval, title` and the rest bound by
283
+ name: PyneCore takes everything after `title` as a keyword. A positional past the last name
284
+ is dropped, as before card #95."""
285
+ names = input_positional_param_names(func_name, positional_args)
286
+ if not names or len(positional_args) <= 2:
287
+ return list(positional_args), {}
288
+ return list(positional_args[:2]), dict(zip(names, positional_args[2:]))
289
+
261
290
 
262
291
  # Pine's positional parameter order of the script declarations (Pine v6 reference; card #55).
263
292
  # The parser binds positionals to these names and codegen emits keywords, so pynecore's own
@@ -16,7 +16,9 @@ statement.
16
16
  declaration (from a function or block too) or in its own declaration.
17
17
  enforce() reports each once per (line, column, name, code) as a
18
18
  SemanticWarning, at its statement, when the script has no refusal; the
19
- output is unchanged.
19
+ output is unchanged. Also a warning, though TradingView compiles it: a
20
+ `ta.*` call that runs on only some bars, whose history differs from the
21
+ every-bar call (conditional_history_calls.py, card #96). Never hoisted.
20
22
 
21
23
  The pass never rewrites the tree, so a script it accepts transpiles exactly as
22
24
  before. An assignment to a history reference (`x[1] := v`) is refused by the
@@ -33,18 +35,19 @@ import warnings
33
35
  from typing import Dict, List, NamedTuple, Optional, Tuple
34
36
 
35
37
  from .ast_nodes import (
36
- ArrayLiteral, Assignment, EnumDecl, Expression, ExpressionStatement, ForInLoop, ForLoop, FuncDecl,
38
+ Assignment, EnumDecl, Expression, ExpressionStatement, ForInLoop, ForLoop, FuncDecl,
37
39
  FunctionCall, Identifier, IfExpression, IfStatement, IndexAccess, InputDecl, Literal, MemberAccess,
38
40
  MethodCall, Reassignment, ReturnStatement, Script, Statement, SwitchExpression,
39
41
  SwitchStatement, TupleDestructure, TypeDecl, VarDecl, VaripDecl, WhileLoop,
40
42
  )
41
43
  from .ast_walk import walk
44
+ from .conditional_history_calls import conditional_history_calls
42
45
  from .errors import SemanticError, SemanticWarning
43
46
  from .library_collision import (
44
47
  FieldReadRule, HeadBinding, declared_type, global_binding_types, script_type_evidence,
45
48
  )
46
49
  from .namespace_members import NAMESPACE_MEMBERS
47
- from .pine_builtins import INPUT_POSITIONAL_PARAMS, PYNECORE_LIB_MODULES, SERIES_VARIABLES
50
+ from .pine_builtins import PYNECORE_LIB_MODULES, SERIES_VARIABLES, input_positional_param_names
48
51
  from .symbol_table import SymbolTable
49
52
  from .udt_field_defaults import constant_heads, default_message, invalid_defaults, positioned_part
50
53
 
@@ -99,10 +102,6 @@ PARAMETER, LOOP_VARIABLE, INPUT, VARIABLE = 'parameter', 'loop variable', 'input
99
102
  STATEMENT_TYPES = typing.get_args(Statement)
100
103
  SCOPE_KIND_TEXT = {'if': 'an if block', 'for': 'a for loop', 'while': 'a while loop',
101
104
  'switch': 'a switch', 'function': 'a function'}
102
- # input.int/input.float have two positional forms after `title` (v6 reference): the minval form
103
- # (INPUT_POSITIONAL_PARAMS) and the options form, selected by a list third argument
104
- NUMERIC_INPUTS = frozenset({'input.int', 'input.float'})
105
- NUMERIC_INPUT_OPTIONS_TAIL = ['options', 'tooltip', 'inline', 'group', 'confirm', 'display']
106
105
  UNDECLARED_MESSAGE = ("'{}' is declared only inside a block or function that has ended here; "
107
106
  "TradingView reports it as undeclared")
108
107
 
@@ -182,6 +181,7 @@ def _pine_version(script: Script) -> int:
182
181
 
183
182
  class _SemanticCheck:
184
183
  def __init__(self, script: Script):
184
+ self.script = script
185
185
  self.version = _pine_version(script)
186
186
  self.constant_heads = constant_heads(script)
187
187
  self.builtin_names = BUILTIN_NAMES | (LEGACY_BUILTIN_VARIABLES if self.version <= LAST_LEGACY_VERSION
@@ -311,8 +311,18 @@ class _SemanticCheck:
311
311
  stack = [self.new_scope('global')]
312
312
  for item in self.top_items:
313
313
  self.statement(item, stack)
314
+ self.conditional_history_calls()
314
315
  return sorted(self.diagnostics, key=lambda diagnostic: (diagnostic.line, diagnostic.column))
315
316
 
317
+ def conditional_history_calls(self) -> None:
318
+ """A `ta.*` call that runs on only some bars keeps a history that differs from the
319
+ every-bar call (card #96). A warning only: the call stays where the script has it."""
320
+ for call in conditional_history_calls(self.script):
321
+ self.report(WARN, 'conditional-history-call', call.func,
322
+ f"'{call.func}' is called only when its condition holds: its series history "
323
+ f'differs from an unconditional call (the output keeps the call where it is, as TradingView '
324
+ f'does; TradingView advises calling it on every bar, in the global scope)', call)
325
+
316
326
  def block(self, statements, stack: List[_Scope], kind: str, names=(), how=LOOP_VARIABLE,
317
327
  types: Optional[Dict[str, Optional[str]]] = None) -> None:
318
328
  inner = stack + [self.new_scope(kind, statements if statements is not None else ())]
@@ -367,10 +377,7 @@ class _SemanticCheck:
367
377
  """An input's arguments must be const, so they cannot read another input (manual,
368
378
  Inputs: "results from other input*() calls"); `active` takes an input bool, and
369
379
  input.source's `defval` a series float (card #89)."""
370
- parameters = ['defval', 'title'] + INPUT_POSITIONAL_PARAMS.get(node.func, [])
371
- if node.func in NUMERIC_INPUTS and len(node.args) > 2 and isinstance(node.args[2], ArrayLiteral):
372
- # the options form of input.int/input.float (pynecore's _bind_numeric_tail)
373
- parameters = ['defval', 'title'] + NUMERIC_INPUT_OPTIONS_TAIL
380
+ parameters = ['defval', 'title'] + input_positional_param_names(node.func, node.args)
374
381
  # a row that does not name `active` ends at `display`: a positional argument after it is `active`
375
382
  positional = (parameters[:parameters.index('display') + 1]
376
383
  if 'display' in parameters and 'active' not in parameters else parameters)
@@ -15,7 +15,9 @@ declaration keep the outer name, as in Pine.
15
15
  A binding named like a pynecore.lib name that the script also uses as that
16
16
  name (`f(color = color.red) => color.r(color)`) is renamed too, together with
17
17
  its uses and the keyword arguments that name it; a declared function shadows
18
- every bare call of its name (card #9). `N.m` keeps the namespace when `m` is
18
+ every bare call of its name (card #9). A function named like a builtin series
19
+ (`hour(x) => x * 3`, `close(x) => ...`) is renamed too whether or not the script reads
20
+ the series: its calls go to the new name and a bare read of the name stays the series (card #97). `N.m` keeps the namespace when `m` is
19
21
  not the binding's own member (see library_collision.MemberRule). Two kinds of
20
22
  binding keep their name: an input (its name is the key of the script's input
21
23
  settings) and an `export` declaration (its name is the library's API); a
@@ -181,7 +183,7 @@ class _ScopeRenamer:
181
183
  # type of `x = mk()` (card #87), `x = array.get(a, 0)` or `x = c ? T.new() : na` (card #91)
182
184
  self.type_evidence = script_type_evidence(script)
183
185
  self.namespace_aliases: Dict[str, str] = {}
184
- # library-named user function -> its emitted name (shared by its overloads)
186
+ # user function named like a library name or a builtin series -> its emitted name (shared by its overloads)
185
187
  self.function_names: Dict[str, str] = {}
186
188
  # function or method name -> {renamed parameter: its emitted name}
187
189
  self.parameter_names: Dict[str, Dict[str, str]] = {}
@@ -437,8 +439,8 @@ class _ScopeRenamer:
437
439
  return_type=self.type_hint(node.return_type))
438
440
 
439
441
  def plan_functions(self, script: Script) -> None:
440
- """Fresh names for library-named user functions (every overload shares
441
- one) and their parameters named like a library name or a script global
442
+ """Fresh names for user functions named like a library name or a builtin
443
+ series (every overload shares one) and their parameters named like a library name or a script global
442
444
  (one per function name, so a keyword argument maps the same way to every
443
445
  overload). An `export` declaration keeps its names."""
444
446
  global_names = script_global_names(script)
@@ -452,7 +454,11 @@ class _ScopeRenamer:
452
454
  self.plan_export_parameters(declaration)
453
455
  continue
454
456
  name = declaration.name
455
- if name in self.library_names and not declaration.is_method and name not in self.function_names:
457
+ # a function named like a pynecore.lib name the script uses, or like a builtin series (card
458
+ # #97): the module imports the builtin wherever its name is read, and pynecore reads the
459
+ # library object for every use of an imported name, the function's calls included
460
+ if ((name in self.library_names or name in SERIES_VARIABLES)
461
+ and not declaration.is_method and name not in self.function_names):
456
462
  self.function_names[name] = self.fresh_name(name)
457
463
  elif name not in self.function_names and (
458
464
  name in PYTHON_RESERVED_WORDS or name in self.external_spellings):
@@ -715,6 +721,6 @@ class _ScopeRenamer:
715
721
  for declaration in script.declarations]
716
722
  body = [self.in_source_order(self.global_statement, statement) for statement in script.body]
717
723
  if not (self.shadowed_names or self.library_names or any(self.parameter_names.values())
718
- or self.spellings.changed or self.moved_bindings or self.moved_inputs):
724
+ or self.spellings.changed or self.moved_bindings or self.moved_inputs or self.function_names):
719
725
  return script
720
726
  return dataclasses.replace(script, declarations=declarations, body=body)