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.
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/PKG-INFO +1 -1
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/PKG-INFO +1 -1
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/SOURCES.txt +6 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/__init__.py +1 -1
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/ast_nodes.py +1 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/codegen.py +3 -2
- opencode_pine2pyne-0.2.1/pine2pyne/conditional_history_calls.py +96 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/errors.py +2 -1
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/import_resolver.py +4 -1
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/parser.py +107 -29
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/pine_builtins.py +31 -2
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/pine_semantics.py +18 -11
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/scope_renamer.py +12 -6
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/transformer.py +23 -21
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pyproject.toml +3 -0
- opencode_pine2pyne-0.2.1/tests/test_assignment_to_a_call_result_field.py +148 -0
- opencode_pine2pyne-0.2.1/tests/test_builtin_named_function_and_enum_input.py +122 -0
- opencode_pine2pyne-0.2.1/tests/test_conditional_history_call_warning.py +127 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_errors.py +3 -0
- opencode_pine2pyne-0.2.1/tests/test_input_numeric_positional_binding.py +97 -0
- opencode_pine2pyne-0.2.1/tests/test_loop_and_array_edge_cases.py +97 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/LICENSE +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/README.md +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/dependency_links.txt +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/entry_points.txt +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/requires.txt +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/top_level.txt +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/README.md +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/TRANSPILER_BEST_PRACTICES.md +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/TRANSPILER_USAGE.md +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/__main__.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/ast_walk.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/block_value_lowering.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/bool_builtins.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/bool_value_typing.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/cli.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/emitted_names.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/for_loop_end_bound.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/grouped_history_lowering.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/lexer.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/library_collision.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/namespace_members.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/once_lowering.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/overload_resolution.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/parse_repair.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/strict_boolean.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/symbol_table.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/tokens.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/type_inference.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/pine2pyne/udt_field_defaults.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/setup.cfg +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_block_tail_value_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_block_value_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_cli.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_codegen.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_codegen_operator_grouping.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_codegen_refusal.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_collect_namespace_members.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_compare_ground_truth_completeness.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_corpus_regression.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_dotted_udt_default_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_drawing_receiver_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_emitted_name_collision_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_emitted_name_plan.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_error_demo_claims.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_error_demo_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_for_in_pairs_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_for_loop_dynamic_end_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_function_tail_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_generated_tables.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_check_no_output.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_check_own_directory.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_digests.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_manifest_order.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_no_output.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_run_dir_isolation.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_settings.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ground_truth_wrapper_retired.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_grouped_history_lowering.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_grouped_history_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_import_resolver.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_input_constant_and_enum_order_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_input_history_read_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_lexer.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_library_name_collision.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_library_name_collision_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_method_namespaces.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_method_receiver_shape_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_method_routing_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_once_structure_parser.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_once_structure_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_overload_dispatch_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_overload_resolution.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_parse_repair_card55.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_parser.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_parser_positions.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_pine_builtins.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_pine_semantics.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_release_artifact_grouping.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_runtime_floor.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_scope_model_drift.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_scope_renamer.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_screener_regression.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_script_declaration_binding.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_shadowing_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_strict_and_or_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_suite_harness_exit_verdict.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_suite_report_compare.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_switch_subject_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_symbol_table.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_ta_conditional_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_transformer.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_transformer_receiver_typing.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_transformer_single_use.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_udt_array_new_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_udt_bool_field_default_by_version_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_udt_field_defaults_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_udt_field_history_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_udt_type_proof.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_untyped_na_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_user_declaration_shadows_builtin_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_v5_bool_cast_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_var_history_runtime.py +0 -0
- {opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/tests/test_variable_function_runtime.py +0 -0
{opencode_pine2pyne-0.2.0 → opencode_pine2pyne-0.2.1}/opencode_pine2pyne.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
@@ -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
|
|
548
|
+
self.emit(f'for {var} in {range_helper}({from_val}, {to_val}, {step}):')
|
|
548
549
|
else:
|
|
549
|
-
self.emit(f'for {var} in
|
|
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
|
|
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
|
|
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
|
|
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) ->
|
|
807
|
-
"""Parse a
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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', '
|
|
248
|
-
'input.float': ['minval', 'maxval', 'step', 'tooltip', 'inline', 'group', 'confirm', 'display', '
|
|
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
|
-
|
|
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
|
|
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'] +
|
|
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).
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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)
|