pine2pyne 0.2.4__py3-none-any.whl
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.
- pine2pyne/__init__.py +84 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +405 -0
- pine2pyne/ast_walk.py +93 -0
- pine2pyne/block_value_lowering.py +694 -0
- pine2pyne/bool_builtins.py +42 -0
- pine2pyne/bool_value_typing.py +580 -0
- pine2pyne/cli.py +247 -0
- pine2pyne/codegen.py +886 -0
- pine2pyne/compat/__init__.py +19 -0
- pine2pyne/compat/__main__.py +6 -0
- pine2pyne/compat/cli.py +114 -0
- pine2pyne/compat/comparison.py +105 -0
- pine2pyne/compat/corpus.py +73 -0
- pine2pyne/compat/report.py +95 -0
- pine2pyne/compat/runner.py +63 -0
- pine2pyne/condition_typing.py +260 -0
- pine2pyne/conditional_history_calls.py +96 -0
- pine2pyne/core/__init__.py +26 -0
- pine2pyne/core/contracts.py +125 -0
- pine2pyne/di.py +173 -0
- pine2pyne/emit/__init__.py +5 -0
- pine2pyne/emit/legacy.py +14 -0
- pine2pyne/emit/pynecomp/__init__.py +20 -0
- pine2pyne/emit/pynecomp/banner.py +30 -0
- pine2pyne/emit/pynecomp/blank_lines.py +69 -0
- pine2pyne/emit/pynecomp/emitter.py +61 -0
- pine2pyne/emit/pynecomp/imports.py +65 -0
- pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
- pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
- pine2pyne/emit/pynecomp/passes/base.py +69 -0
- pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
- pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
- pine2pyne/emit/pynecomp/passes/imports.py +112 -0
- pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
- pine2pyne/emit/pynecomp/passes/library.py +205 -0
- pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
- pine2pyne/emit/pynecomp/printer.py +288 -0
- pine2pyne/emit/pynecomp/unparser.py +186 -0
- pine2pyne/emitted_names.py +152 -0
- pine2pyne/errors.py +69 -0
- pine2pyne/for_loop_end_bound.py +172 -0
- pine2pyne/grouped_history_lowering.py +310 -0
- pine2pyne/import_resolver.py +469 -0
- pine2pyne/input_hoisting.py +195 -0
- pine2pyne/legacy_function_names.py +74 -0
- pine2pyne/legacy_sessions.py +68 -0
- pine2pyne/lexer.py +630 -0
- pine2pyne/library_collision.py +434 -0
- pine2pyne/namespace_members.py +247 -0
- pine2pyne/node_ids.py +235 -0
- pine2pyne/once_lowering.py +75 -0
- pine2pyne/overload_resolution.py +475 -0
- pine2pyne/parse_repair.py +398 -0
- pine2pyne/parser.py +1873 -0
- pine2pyne/pine_builtins.py +394 -0
- pine2pyne/pine_semantics.py +604 -0
- pine2pyne/pipeline/__init__.py +18 -0
- pine2pyne/pipeline/service.py +70 -0
- pine2pyne/pipeline/stages.py +73 -0
- pine2pyne/scope_renamer.py +965 -0
- pine2pyne/strategy_when_lowering.py +101 -0
- pine2pyne/strict_boolean.py +101 -0
- pine2pyne/symbol_table.py +166 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2510 -0
- pine2pyne/type_inference.py +422 -0
- pine2pyne/udt_field_defaults.py +149 -0
- pine2pyne/value_blocks.py +357 -0
- pine2pyne-0.2.4.dist-info/METADATA +99 -0
- pine2pyne-0.2.4.dist-info/RECORD +75 -0
- pine2pyne-0.2.4.dist-info/WHEEL +5 -0
- pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
- pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
- pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
pine2pyne/codegen.py
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code generator: Emit Python source code from transformed AST.
|
|
3
|
+
|
|
4
|
+
Generates PEP 8 compliant Python code compatible with PyneCore.
|
|
5
|
+
"""
|
|
6
|
+
import re
|
|
7
|
+
from typing import Dict, List, Union
|
|
8
|
+
from .ast_nodes import *
|
|
9
|
+
from .transformer import PyneTransformedScript
|
|
10
|
+
from .errors import CodeGenError
|
|
11
|
+
from .udt_field_defaults import field_value
|
|
12
|
+
from .ast_walk import walk
|
|
13
|
+
from .strict_boolean import strict_operands
|
|
14
|
+
|
|
15
|
+
# Pine binary operator precedence, lowest to highest, mirroring the parser's
|
|
16
|
+
# chain parse_or -> parse_multiplication. Every level is left-associative.
|
|
17
|
+
BINARY_OPERATOR_PRECEDENCE = {
|
|
18
|
+
'or': 1,
|
|
19
|
+
'and': 2,
|
|
20
|
+
'==': 3, '!=': 3,
|
|
21
|
+
'<': 4, '>': 4, '<=': 4, '>=': 4,
|
|
22
|
+
'+': 5, '-': 5,
|
|
23
|
+
'*': 6, '/': 6, '%': 6,
|
|
24
|
+
}
|
|
25
|
+
COMPARISON_OPERATORS = frozenset({'==', '!=', '<', '>', '<=', '>='})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CodeGenerator:
|
|
29
|
+
"""Generates Python source code from transformed AST."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, indent_size: int = 4, keep_keyword_string_quotes: bool = False):
|
|
32
|
+
self.indent_size = indent_size
|
|
33
|
+
# False: a keyword argument's string is double-quoted (pine2pyne's layout);
|
|
34
|
+
# True: it keeps the quotes of the Pine source (what PyneComp prints)
|
|
35
|
+
self.keep_keyword_string_quotes = keep_keyword_string_quotes
|
|
36
|
+
self.indent_level = 0
|
|
37
|
+
self.output: List[str] = []
|
|
38
|
+
# The Pine line of the top-level statement being emitted. The parser
|
|
39
|
+
# stamps only top-level statements, so a refusal of a nested node
|
|
40
|
+
# reports this line (card #40)
|
|
41
|
+
self.top_level_line = 0
|
|
42
|
+
# Pine v5 and earlier evaluate both operands of and/or (card #65)
|
|
43
|
+
self.strict_boolean_operators = False
|
|
44
|
+
# Pine v6 gives a UDT bool field without a default false; before v6 it is na (card #76)
|
|
45
|
+
self.bool_is_never_na = True
|
|
46
|
+
self.user_function_names: frozenset = frozenset()
|
|
47
|
+
# Output line index -> the Pine line its statement came from. A layout
|
|
48
|
+
# printer (pine2pyne.emit) reads it to place blank lines the way the source does
|
|
49
|
+
self.line_origins: Dict[int, int] = {}
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _esc(name: str) -> str:
|
|
53
|
+
"""Escape Python reserved words used as identifiers."""
|
|
54
|
+
from .pine_builtins import sanitize_identifier
|
|
55
|
+
return sanitize_identifier(name)
|
|
56
|
+
|
|
57
|
+
def _get_all_type_hints(self, script) -> List[str]:
|
|
58
|
+
"""Extract all type hints from script for Any detection."""
|
|
59
|
+
hints = []
|
|
60
|
+
|
|
61
|
+
# Check main parameters
|
|
62
|
+
for param in script.main_params:
|
|
63
|
+
if param.type_hint:
|
|
64
|
+
hints.append(param.type_hint)
|
|
65
|
+
|
|
66
|
+
# Check main body statements
|
|
67
|
+
for stmt in script.main_body:
|
|
68
|
+
if hasattr(stmt, 'type_hint') and stmt.type_hint:
|
|
69
|
+
hints.append(stmt.type_hint)
|
|
70
|
+
|
|
71
|
+
# Check global vars
|
|
72
|
+
for var in script.global_vars:
|
|
73
|
+
if hasattr(var, 'type_hint') and var.type_hint:
|
|
74
|
+
hints.append(var.type_hint)
|
|
75
|
+
|
|
76
|
+
# Check helper functions
|
|
77
|
+
for func in script.helper_functions:
|
|
78
|
+
if func.return_type:
|
|
79
|
+
hints.append(func.return_type)
|
|
80
|
+
for param in func.params:
|
|
81
|
+
if param.type_hint:
|
|
82
|
+
hints.append(param.type_hint)
|
|
83
|
+
# Check function body (handle both list and single statement)
|
|
84
|
+
body = func.body if isinstance(func.body, list) else [func.body]
|
|
85
|
+
for stmt in body:
|
|
86
|
+
if hasattr(stmt, 'type_hint') and stmt.type_hint:
|
|
87
|
+
hints.append(stmt.type_hint)
|
|
88
|
+
|
|
89
|
+
return hints
|
|
90
|
+
|
|
91
|
+
def generate(self, script: PyneTransformedScript) -> str:
|
|
92
|
+
"""Generate complete Python source code."""
|
|
93
|
+
self.output = []
|
|
94
|
+
self.line_origins = {}
|
|
95
|
+
self.top_level_line = 0
|
|
96
|
+
self.strict_boolean_operators = script.strict_boolean_operators
|
|
97
|
+
self.bool_is_never_na = script.bool_is_never_na
|
|
98
|
+
self.user_function_names = frozenset(
|
|
99
|
+
node.name for node in walk([script.helper_functions, script.main_body, script.global_vars])
|
|
100
|
+
if isinstance(node, FuncDecl))
|
|
101
|
+
|
|
102
|
+
# 1. @pyne magic docstring with attribution
|
|
103
|
+
self.emit('"""')
|
|
104
|
+
self.emit('@pyne')
|
|
105
|
+
self.emit('')
|
|
106
|
+
self.emit('This code was compiled by Pine2Pyne — the Pine Script to PyneCore\'s Python compiler.')
|
|
107
|
+
self.emit('"""')
|
|
108
|
+
self.emit('')
|
|
109
|
+
|
|
110
|
+
# 2. Import statements
|
|
111
|
+
# Check if Any is used in type hints
|
|
112
|
+
uses_any = any('[Any]' in stmt or ': Any' in stmt for stmt in self._get_all_type_hints(script))
|
|
113
|
+
|
|
114
|
+
if uses_any:
|
|
115
|
+
self.emit('from typing import Any')
|
|
116
|
+
|
|
117
|
+
for import_stmt in script.imports:
|
|
118
|
+
self.emit(import_stmt)
|
|
119
|
+
if script.imports or uses_any:
|
|
120
|
+
self.emit('')
|
|
121
|
+
|
|
122
|
+
# 3-5. Module prelude in three phases (card #78): enum classes, then the module
|
|
123
|
+
# constants (they may read an enum member), then the UDT classes (a field default may
|
|
124
|
+
# read an enum member, and cannot read a constant)
|
|
125
|
+
# Enum declarations
|
|
126
|
+
for enum_decl in script.enum_declarations:
|
|
127
|
+
self.top_level_line = enum_decl.line
|
|
128
|
+
self.generate_enum_declaration(enum_decl)
|
|
129
|
+
self.emit('')
|
|
130
|
+
|
|
131
|
+
# Module-level constants (e.g., const string group labels, pre-main literals)
|
|
132
|
+
# These must be at module level because they are referenced by main()
|
|
133
|
+
# parameter defaults which Python evaluates at function definition time.
|
|
134
|
+
if script.module_constants:
|
|
135
|
+
for const_stmt in script.module_constants:
|
|
136
|
+
self.generate_top_level_statement(const_stmt)
|
|
137
|
+
self.emit('')
|
|
138
|
+
|
|
139
|
+
# Type declarations (custom types with @udt)
|
|
140
|
+
self._udt_names = {type_decl.name for type_decl in script.type_declarations}
|
|
141
|
+
# Pine v6: a gap in the source between two fields is a blank line between them (PyneComp)
|
|
142
|
+
self._keeps_field_gaps = getattr(script, 'pine_version', 6) >= 6
|
|
143
|
+
for type_decl in script.type_declarations:
|
|
144
|
+
self.top_level_line = type_decl.line
|
|
145
|
+
self.generate_type_declaration(type_decl)
|
|
146
|
+
self.emit('')
|
|
147
|
+
|
|
148
|
+
# A library's export surface: one Exported proxy per exported name, published in __all__ (card #111).
|
|
149
|
+
# pynecore's `@export` resolves the proxy by name in this module's globals
|
|
150
|
+
exported_names = list(dict.fromkeys(
|
|
151
|
+
statement.name for statement in script.main_body if isinstance(statement, FuncDecl) and statement.is_export))
|
|
152
|
+
if exported_names:
|
|
153
|
+
for exported_name in exported_names:
|
|
154
|
+
self.emit(f'{self._esc(exported_name)} = Exported()')
|
|
155
|
+
self.emit('')
|
|
156
|
+
self.emit('__all__ = [' + ', '.join(f"'{self._esc(name)}'" for name in exported_names) + ']')
|
|
157
|
+
self.emit('')
|
|
158
|
+
|
|
159
|
+
# 4. Main function with decorator (helper functions now inside main)
|
|
160
|
+
if script.main_decorator:
|
|
161
|
+
self.top_level_line = script.main_decorator.line
|
|
162
|
+
decorator = self.generate_decorator(script.main_decorator)
|
|
163
|
+
self.emit(decorator)
|
|
164
|
+
|
|
165
|
+
# Generate main function signature
|
|
166
|
+
self.emit('def main(')
|
|
167
|
+
self.indent_level += 1
|
|
168
|
+
|
|
169
|
+
# Generate parameters
|
|
170
|
+
if script.main_params:
|
|
171
|
+
for i, param in enumerate(script.main_params):
|
|
172
|
+
self.top_level_line = param.line # the Pine input it came from
|
|
173
|
+
param_str = self.generate_parameter(param)
|
|
174
|
+
if i < len(script.main_params) - 1:
|
|
175
|
+
param_str += ','
|
|
176
|
+
self.emit(param_str)
|
|
177
|
+
else:
|
|
178
|
+
# No parameters - but Python requires at least empty ()
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
self.indent_level -= 1
|
|
182
|
+
|
|
183
|
+
# Close parameter list and add colon
|
|
184
|
+
if script.main_params:
|
|
185
|
+
self.emit('):')
|
|
186
|
+
else:
|
|
187
|
+
self.output[-1] += '):' # Add closing paren and colon to def line
|
|
188
|
+
|
|
189
|
+
self.indent_level += 1
|
|
190
|
+
|
|
191
|
+
main_body_start = len(self.output)
|
|
192
|
+
|
|
193
|
+
# Generate global var declarations (Persistent)
|
|
194
|
+
for var_decl in script.global_vars:
|
|
195
|
+
self.generate_top_level_statement(var_decl)
|
|
196
|
+
|
|
197
|
+
# Generate main body with blank lines between statements
|
|
198
|
+
# Rule: add blank line after function defs and between different statement kinds
|
|
199
|
+
prev_kind = None # 'persistent', 'func', 'body'
|
|
200
|
+
prev_source_line = 0
|
|
201
|
+
for i, stmt in enumerate(script.main_body):
|
|
202
|
+
source_line = getattr(stmt, 'line', 0)
|
|
203
|
+
|
|
204
|
+
# Determine statement kind
|
|
205
|
+
if isinstance(stmt, Assignment) and isinstance(stmt.type_hint, str) and 'Persistent' in stmt.type_hint:
|
|
206
|
+
kind = 'persistent'
|
|
207
|
+
elif isinstance(stmt, FuncDecl):
|
|
208
|
+
kind = 'func'
|
|
209
|
+
else:
|
|
210
|
+
kind = 'body'
|
|
211
|
+
|
|
212
|
+
# Add blank lines based on context
|
|
213
|
+
if i > 0:
|
|
214
|
+
gap = source_line - prev_source_line if source_line > 0 and prev_source_line > 0 else 0
|
|
215
|
+
|
|
216
|
+
if prev_kind == 'func':
|
|
217
|
+
# Always blank line after function definition
|
|
218
|
+
self.emit('')
|
|
219
|
+
elif prev_kind != kind:
|
|
220
|
+
# Transition between statement types
|
|
221
|
+
if gap > 5:
|
|
222
|
+
# Large gap (section change) - add extra blank lines
|
|
223
|
+
blanks = min((gap - 1) // 10 + 1, 3)
|
|
224
|
+
for _ in range(blanks):
|
|
225
|
+
self.emit('')
|
|
226
|
+
else:
|
|
227
|
+
self.emit('')
|
|
228
|
+
elif kind == 'persistent' and gap >= 3:
|
|
229
|
+
# Gap between Persistent groups (e.g., maps → arrays)
|
|
230
|
+
self.emit('')
|
|
231
|
+
elif kind == 'body' and gap >= 3:
|
|
232
|
+
# Gap between body statements (comment/blank line in source)
|
|
233
|
+
self.emit('')
|
|
234
|
+
|
|
235
|
+
self.generate_top_level_statement(stmt)
|
|
236
|
+
prev_kind = kind
|
|
237
|
+
prev_source_line = source_line
|
|
238
|
+
|
|
239
|
+
# A script with no executable statement (a type-only or comment-only
|
|
240
|
+
# Pine script) still needs a statement under `def main():` (card #15)
|
|
241
|
+
if len(self.output) == main_body_start:
|
|
242
|
+
self.emit('pass')
|
|
243
|
+
|
|
244
|
+
self.indent_level -= 1
|
|
245
|
+
|
|
246
|
+
return '\n'.join(self.output)
|
|
247
|
+
|
|
248
|
+
# ========================================================================
|
|
249
|
+
# Emission helpers
|
|
250
|
+
# ========================================================================
|
|
251
|
+
|
|
252
|
+
def emit(self, line: str = '') -> None:
|
|
253
|
+
"""Emit a line of code with current indentation."""
|
|
254
|
+
if line:
|
|
255
|
+
indent = ' ' * (self.indent_level * self.indent_size)
|
|
256
|
+
self.output.append(indent + line)
|
|
257
|
+
else:
|
|
258
|
+
self.output.append('')
|
|
259
|
+
|
|
260
|
+
def emit_inline(self, text: str) -> None:
|
|
261
|
+
"""Emit text without newline (append to last line)."""
|
|
262
|
+
if self.output:
|
|
263
|
+
self.output[-1] += text
|
|
264
|
+
else:
|
|
265
|
+
self.output.append(text)
|
|
266
|
+
|
|
267
|
+
# ========================================================================
|
|
268
|
+
# Top-level code generation
|
|
269
|
+
# ========================================================================
|
|
270
|
+
|
|
271
|
+
def generate_decorator(self, decl: Union[IndicatorDecl, StrategyDecl, LibraryDecl]) -> str:
|
|
272
|
+
"""Generate @script.indicator, @script.strategy or @script.library decorator."""
|
|
273
|
+
name = {IndicatorDecl: 'indicator', LibraryDecl: 'library'}.get(type(decl), 'strategy')
|
|
274
|
+
# A None title is an expression, emitted as the 'title' keyword (card #55)
|
|
275
|
+
arguments = [] if decl.title is None else [f"'{self._escape_string(decl.title, chr(39))}'"]
|
|
276
|
+
arguments += [f'{key}={self.generate_decorator_value(value)}' for key, value in decl.kwargs.items()]
|
|
277
|
+
return f"@script.{name}({', '.join(arguments)})"
|
|
278
|
+
|
|
279
|
+
def generate_decorator_value(self, value: Any) -> str:
|
|
280
|
+
"""Generate decorator kwarg value."""
|
|
281
|
+
if isinstance(value, bool):
|
|
282
|
+
return 'True' if value else 'False'
|
|
283
|
+
elif isinstance(value, (int, float)):
|
|
284
|
+
return str(value)
|
|
285
|
+
elif isinstance(value, str):
|
|
286
|
+
return f'"{value}"'
|
|
287
|
+
elif isinstance(value, Literal):
|
|
288
|
+
return self.generate_literal(value)
|
|
289
|
+
elif isinstance(value, MemberAccess):
|
|
290
|
+
return self.generate_member_access(value)
|
|
291
|
+
elif isinstance(value, Identifier):
|
|
292
|
+
return value.name
|
|
293
|
+
elif isinstance(value, Expression):
|
|
294
|
+
# Handle any other expression type
|
|
295
|
+
return self.generate_expression(value)
|
|
296
|
+
else:
|
|
297
|
+
return str(value)
|
|
298
|
+
|
|
299
|
+
def generate_parameter(self, param: Parameter) -> str:
|
|
300
|
+
"""Generate function parameter."""
|
|
301
|
+
param_str = self._esc(param.name)
|
|
302
|
+
|
|
303
|
+
# Add type hint
|
|
304
|
+
if param.type_hint:
|
|
305
|
+
param_str += f': {param.type_hint}'
|
|
306
|
+
|
|
307
|
+
# Add default value
|
|
308
|
+
if param.default:
|
|
309
|
+
default_str = self.generate_expression(param.default)
|
|
310
|
+
# Fix NA() to na for function default parameters
|
|
311
|
+
if default_str == 'NA()':
|
|
312
|
+
default_str = 'na'
|
|
313
|
+
# PEP 8: spaces around = in defaults when type annotation present
|
|
314
|
+
if param.type_hint:
|
|
315
|
+
param_str += f' = {default_str}'
|
|
316
|
+
else:
|
|
317
|
+
param_str += f'={default_str}'
|
|
318
|
+
|
|
319
|
+
return param_str
|
|
320
|
+
|
|
321
|
+
# ========================================================================
|
|
322
|
+
# Type declaration generation
|
|
323
|
+
# ========================================================================
|
|
324
|
+
|
|
325
|
+
def generate_type_declaration(self, type_decl: TypeDecl) -> None:
|
|
326
|
+
"""Generate custom type definition with @udt decorator."""
|
|
327
|
+
from .transformer import convert_pine_generic_to_python
|
|
328
|
+
from .pine_builtins import get_type_name
|
|
329
|
+
|
|
330
|
+
# Emit @udt decorator
|
|
331
|
+
self.emit('@udt')
|
|
332
|
+
self.emit(f'class {self._esc(type_decl.name)}:')
|
|
333
|
+
self.indent_level += 1
|
|
334
|
+
|
|
335
|
+
# Generate fields: a series built-in default is read per instance, a
|
|
336
|
+
# field without a default is na (card #17, udt_field_defaults)
|
|
337
|
+
lines = list(getattr(type_decl, 'field_lines', ()) or ())
|
|
338
|
+
for index, (field_name, field_type, default_value) in enumerate(type_decl.fields):
|
|
339
|
+
if getattr(self, '_keeps_field_gaps', False) and 0 < index < len(lines) and lines[index] - lines[index - 1] > 1:
|
|
340
|
+
self.emit('')
|
|
341
|
+
# Convert Pine type to Python type (handles generics like array<T> → list[T])
|
|
342
|
+
py_type = convert_pine_generic_to_python(field_type)
|
|
343
|
+
fname = self._esc(field_name)
|
|
344
|
+
|
|
345
|
+
# For NA() calls, use base type only (NA(list) not NA(list[X]))
|
|
346
|
+
na_type = py_type.split('[')[0] if '[' in py_type else py_type
|
|
347
|
+
|
|
348
|
+
# A field that holds a UDT (its own class, or one declared later) is annotated with a
|
|
349
|
+
# string, and its na is made per instance (PyneComp: `p: 'P' = field(default_factory=...)`)
|
|
350
|
+
self_referential = re.search(rf'\b{re.escape(type_decl.name)}\b', py_type) is not None
|
|
351
|
+
holds_a_udt = na_type in getattr(self, '_udt_names', ()) or na_type == type_decl.name
|
|
352
|
+
# and so is a collection of one (`tp: 'list[PosAction]' = na(list)`); an enum is not
|
|
353
|
+
mentions_a_udt = any(re.search(rf'\b{re.escape(name)}\b', py_type) for name in getattr(self, '_udt_names', ()))
|
|
354
|
+
annotation = f"'{py_type}'" if self_referential or holds_a_udt or mentions_a_udt else py_type
|
|
355
|
+
generated_default = (self.generate_expression(default_value)
|
|
356
|
+
if default_value is not None and not isinstance(default_value, NaLiteral) else None)
|
|
357
|
+
value = field_value(py_type, na_type, default_value, generated_default, holds_a_udt,
|
|
358
|
+
self.bool_is_never_na)
|
|
359
|
+
self.emit(f'{fname}: {annotation} = {value}')
|
|
360
|
+
|
|
361
|
+
self.indent_level -= 1
|
|
362
|
+
|
|
363
|
+
def generate_enum_declaration(self, enum_decl: EnumDecl) -> None:
|
|
364
|
+
"""Generate an enum as PyneComp does: a `StrEnum` (imported as `Enum`) whose members
|
|
365
|
+
are their titles, a member without one its own name (`a = 'a'`)."""
|
|
366
|
+
self.uses_enum = True
|
|
367
|
+
self.emit(f'class {self._esc(enum_decl.name)}(Enum):')
|
|
368
|
+
self.indent_level += 1
|
|
369
|
+
|
|
370
|
+
for member in enum_decl.members:
|
|
371
|
+
if isinstance(member, tuple):
|
|
372
|
+
name, value = member
|
|
373
|
+
value_str = (repr(value.value) if isinstance(value, Literal) and isinstance(value.value, str)
|
|
374
|
+
else self.generate_expression(value))
|
|
375
|
+
self.emit(f'{self._esc(name)} = {value_str}')
|
|
376
|
+
else:
|
|
377
|
+
# No explicit value — use member name as string value
|
|
378
|
+
self.emit(f'{self._esc(member)} = {member!r}')
|
|
379
|
+
|
|
380
|
+
self.indent_level -= 1
|
|
381
|
+
|
|
382
|
+
# ========================================================================
|
|
383
|
+
# Function generation
|
|
384
|
+
# ========================================================================
|
|
385
|
+
|
|
386
|
+
def generate_function(self, func: FuncDecl) -> None:
|
|
387
|
+
"""Generate function definition."""
|
|
388
|
+
# Emit @method decorator if this is a method
|
|
389
|
+
if hasattr(func, 'is_method') and func.is_method:
|
|
390
|
+
self.emit('@method')
|
|
391
|
+
# A library's exported function (card #111); pynecore's @export goes above @overload
|
|
392
|
+
if func.is_export:
|
|
393
|
+
self.emit('@export')
|
|
394
|
+
# Same-name declarations share one pynecore @overload dispatcher (card #39)
|
|
395
|
+
if func.is_overload:
|
|
396
|
+
self.emit('@overload')
|
|
397
|
+
|
|
398
|
+
# Function signature
|
|
399
|
+
params = ', '.join(self.generate_parameter(p) for p in func.params)
|
|
400
|
+
self.emit(f'def {self._esc(func.name)}({params}):')
|
|
401
|
+
|
|
402
|
+
self.indent_level += 1
|
|
403
|
+
|
|
404
|
+
# Track if we emitted any body content
|
|
405
|
+
body_was_empty = False
|
|
406
|
+
|
|
407
|
+
# Function body
|
|
408
|
+
if isinstance(func.body, (list, tuple)):
|
|
409
|
+
for i, stmt in enumerate(func.body):
|
|
410
|
+
is_last = (i == len(func.body) - 1)
|
|
411
|
+
# Last expression in body is the return value
|
|
412
|
+
if is_last and isinstance(stmt, Expression):
|
|
413
|
+
self.emit(f'return {self.generate_expression(stmt)}')
|
|
414
|
+
else:
|
|
415
|
+
self.generate_statement(stmt)
|
|
416
|
+
# If body list was empty or all statements were None, add pass
|
|
417
|
+
if not func.body or all(s is None for s in func.body):
|
|
418
|
+
self.emit('pass')
|
|
419
|
+
body_was_empty = True
|
|
420
|
+
elif isinstance(func.body, Expression):
|
|
421
|
+
# Single expression - add return
|
|
422
|
+
self.emit(f'return {self.generate_expression(func.body)}')
|
|
423
|
+
elif isinstance(func.body, ReturnStatement):
|
|
424
|
+
self.generate_statement(func.body)
|
|
425
|
+
else:
|
|
426
|
+
# Empty or None body - add pass
|
|
427
|
+
self.emit('pass')
|
|
428
|
+
body_was_empty = True
|
|
429
|
+
|
|
430
|
+
self.indent_level -= 1
|
|
431
|
+
|
|
432
|
+
# ========================================================================
|
|
433
|
+
# Statement generation
|
|
434
|
+
# ========================================================================
|
|
435
|
+
|
|
436
|
+
def generate_top_level_statement(self, stmt: Statement) -> None:
|
|
437
|
+
"""Generate a statement of the module or of main(), recording its Pine line."""
|
|
438
|
+
self.top_level_line = getattr(stmt, 'line', 0)
|
|
439
|
+
self.generate_statement(stmt)
|
|
440
|
+
|
|
441
|
+
def refusal_line(self, node: ASTNode) -> int:
|
|
442
|
+
"""The Pine line to report for a node codegen refuses: its own, or its
|
|
443
|
+
top-level statement's when the parser did not stamp it."""
|
|
444
|
+
return getattr(node, 'line', 0) or self.top_level_line
|
|
445
|
+
|
|
446
|
+
def generate_statement(self, stmt: Statement) -> None:
|
|
447
|
+
"""Generate a statement."""
|
|
448
|
+
# Flatten lists of statements (from transforms that expand one stmt into multiple)
|
|
449
|
+
if isinstance(stmt, list):
|
|
450
|
+
for s in stmt:
|
|
451
|
+
self.generate_statement(s)
|
|
452
|
+
return
|
|
453
|
+
pine_line = getattr(stmt, 'line', 0) or getattr(stmt, 'source_line', 0)
|
|
454
|
+
if pine_line:
|
|
455
|
+
self.line_origins.setdefault(len(self.output), pine_line)
|
|
456
|
+
if isinstance(stmt, FuncDecl):
|
|
457
|
+
# Functions can now appear inside main()
|
|
458
|
+
self.generate_function(stmt)
|
|
459
|
+
self.emit('') # Add blank line after function
|
|
460
|
+
elif isinstance(stmt, Assignment):
|
|
461
|
+
self.generate_assignment(stmt)
|
|
462
|
+
elif isinstance(stmt, IfStatement):
|
|
463
|
+
self.generate_if_statement(stmt)
|
|
464
|
+
elif isinstance(stmt, ForLoop):
|
|
465
|
+
self.generate_for_loop(stmt)
|
|
466
|
+
elif isinstance(stmt, ForInLoop):
|
|
467
|
+
self.generate_for_in_loop(stmt)
|
|
468
|
+
elif isinstance(stmt, WhileLoop):
|
|
469
|
+
self.generate_while_loop(stmt)
|
|
470
|
+
elif isinstance(stmt, BreakStatement):
|
|
471
|
+
self.emit('break')
|
|
472
|
+
elif isinstance(stmt, ContinueStatement):
|
|
473
|
+
self.emit('continue')
|
|
474
|
+
elif isinstance(stmt, ReturnStatement):
|
|
475
|
+
self.generate_return_statement(stmt)
|
|
476
|
+
elif isinstance(stmt, ExpressionStatement):
|
|
477
|
+
self.emit(self.generate_expression(stmt.expr))
|
|
478
|
+
elif isinstance(stmt, RawCode):
|
|
479
|
+
for line in stmt.code.split('\n'):
|
|
480
|
+
self.emit(line)
|
|
481
|
+
else:
|
|
482
|
+
# Refuse rather than drop the statement: a comment in its place
|
|
483
|
+
# changes what the program computes, or leaves a block empty (card #40)
|
|
484
|
+
raise CodeGenError(f'Unsupported statement type: {type(stmt).__name__}',
|
|
485
|
+
line=self.refusal_line(stmt))
|
|
486
|
+
|
|
487
|
+
def generate_assignment(self, stmt: Assignment) -> None:
|
|
488
|
+
"""Generate assignment statement."""
|
|
489
|
+
from .ast_nodes import BinaryOp, Identifier, MemberAccess, TupleDestructure
|
|
490
|
+
|
|
491
|
+
# Handle tuple destructuring: [a, b, c] = expr
|
|
492
|
+
if isinstance(stmt.target, TupleDestructure):
|
|
493
|
+
names = ', '.join(self._esc(n) for n in stmt.target.names)
|
|
494
|
+
value = self.generate_expression(stmt.value)
|
|
495
|
+
type_hint = f': {stmt.type_hint}' if stmt.type_hint else ''
|
|
496
|
+
self.emit(f'{names}{type_hint} = {value}')
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
target = self._esc(stmt.target if isinstance(stmt.target, str) else str(stmt.target))
|
|
500
|
+
|
|
501
|
+
# A compound assignment the source wrote as one (x += y), not x := x + y
|
|
502
|
+
if isinstance(stmt.value, BinaryOp) and stmt.value.compound and not stmt.type_hint:
|
|
503
|
+
left_matches = False
|
|
504
|
+
|
|
505
|
+
# Check if the left side of the BinaryOp matches the target
|
|
506
|
+
if isinstance(stmt.value.left, Identifier):
|
|
507
|
+
left_matches = (stmt.value.left.name == target)
|
|
508
|
+
elif isinstance(stmt.value.left, MemberAccess):
|
|
509
|
+
# For member access like this.wins, reconstruct the full name
|
|
510
|
+
left_name = f"{stmt.value.left.object}.{stmt.value.left.member}"
|
|
511
|
+
left_matches = (left_name == target)
|
|
512
|
+
|
|
513
|
+
# If it's a compound assignment pattern, generate +=, -=, etc.
|
|
514
|
+
if left_matches and stmt.value.op in ['+', '-', '*', '/', '%']:
|
|
515
|
+
compound_op = f'{stmt.value.op}='
|
|
516
|
+
right_value = self.generate_expression(stmt.value.right)
|
|
517
|
+
self.emit(f'{target} {compound_op} {right_value}')
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
# Regular assignment
|
|
521
|
+
value = self.generate_expression(stmt.value)
|
|
522
|
+
|
|
523
|
+
# Wrap string literals assigned to Color type as Color('...')
|
|
524
|
+
if stmt.type_hint == 'Color' and value.startswith("'") and value.endswith("'"):
|
|
525
|
+
value = f'Color({value})'
|
|
526
|
+
|
|
527
|
+
# Add type hint if present
|
|
528
|
+
if stmt.type_hint:
|
|
529
|
+
self.emit(f'{target}: {stmt.type_hint} = {value}')
|
|
530
|
+
else:
|
|
531
|
+
self.emit(f'{target} = {value}')
|
|
532
|
+
|
|
533
|
+
def generate_if_statement(self, stmt: IfStatement) -> None:
|
|
534
|
+
"""Generate if statement."""
|
|
535
|
+
condition = self.generate_expression(stmt.condition)
|
|
536
|
+
self.emit(f'if {condition}:')
|
|
537
|
+
|
|
538
|
+
self.indent_level += 1
|
|
539
|
+
for s in stmt.body:
|
|
540
|
+
self.generate_statement(s)
|
|
541
|
+
self.indent_level -= 1
|
|
542
|
+
|
|
543
|
+
# Generate elif clauses
|
|
544
|
+
for elif_cond, elif_body in stmt.elseifs:
|
|
545
|
+
elif_condition = self.generate_expression(elif_cond)
|
|
546
|
+
self.emit(f'elif {elif_condition}:')
|
|
547
|
+
self.indent_level += 1
|
|
548
|
+
for s in elif_body:
|
|
549
|
+
self.generate_statement(s)
|
|
550
|
+
self.indent_level -= 1
|
|
551
|
+
|
|
552
|
+
# Generate else clause
|
|
553
|
+
if stmt.else_body:
|
|
554
|
+
self.emit('else:')
|
|
555
|
+
self.indent_level += 1
|
|
556
|
+
for s in stmt.else_body:
|
|
557
|
+
self.generate_statement(s)
|
|
558
|
+
self.indent_level -= 1
|
|
559
|
+
|
|
560
|
+
def generate_for_loop(self, stmt: ForLoop) -> None:
|
|
561
|
+
"""Generate for loop with pine_range(), or with pine_loop() when the end
|
|
562
|
+
bound is re-read on every pass (Pine v6, card #82)."""
|
|
563
|
+
# Convert Pine 'for i = from to to_val [by step]' to Python pine_range()
|
|
564
|
+
from_val = self.generate_expression(stmt.from_val)
|
|
565
|
+
to_val = self.generate_expression(stmt.to_val)
|
|
566
|
+
|
|
567
|
+
# Pine's 'to' is inclusive, pine_range handles this correctly
|
|
568
|
+
# No need to add 1 like we did with range()
|
|
569
|
+
var = self._esc(stmt.var)
|
|
570
|
+
if stmt.counter_name:
|
|
571
|
+
# from and by are evaluated once, the end before every pass
|
|
572
|
+
step = f', {self.generate_expression(stmt.step)}' if stmt.step else ''
|
|
573
|
+
self.emit(f'{stmt.counter_name} = {stmt.counter_helper}({from_val}{step})')
|
|
574
|
+
self.emit(f'while {stmt.counter_name}.step({to_val}):')
|
|
575
|
+
self.indent_level += 1
|
|
576
|
+
self.emit(f'{var} = {stmt.counter_name}.value')
|
|
577
|
+
for s in stmt.body:
|
|
578
|
+
self.generate_statement(s)
|
|
579
|
+
self.indent_level -= 1
|
|
580
|
+
return
|
|
581
|
+
range_helper = stmt.range_helper or 'pine_range'
|
|
582
|
+
if stmt.step:
|
|
583
|
+
step = self.generate_expression(stmt.step)
|
|
584
|
+
self.emit(f'for {var} in {range_helper}({from_val}, {to_val}, {step}):')
|
|
585
|
+
else:
|
|
586
|
+
self.emit(f'for {var} in {range_helper}({from_val}, {to_val}):')
|
|
587
|
+
|
|
588
|
+
self.indent_level += 1
|
|
589
|
+
for s in stmt.body:
|
|
590
|
+
self.generate_statement(s)
|
|
591
|
+
self.indent_level -= 1
|
|
592
|
+
|
|
593
|
+
def generate_for_in_loop(self, stmt: ForInLoop) -> None:
|
|
594
|
+
"""Generate for-in loop."""
|
|
595
|
+
vars_str = ', '.join(self._esc(v) for v in stmt.vars)
|
|
596
|
+
iterable = self.generate_expression(stmt.iterable)
|
|
597
|
+
|
|
598
|
+
# `for [k, v] in x` reaches here with its pairs already chosen by the
|
|
599
|
+
# transformer: enumerate(), a map's keys, or a run-time choice (card #17)
|
|
600
|
+
self.emit(f'for {vars_str} in {iterable}:')
|
|
601
|
+
|
|
602
|
+
self.indent_level += 1
|
|
603
|
+
for s in stmt.body:
|
|
604
|
+
self.generate_statement(s)
|
|
605
|
+
self.indent_level -= 1
|
|
606
|
+
|
|
607
|
+
def generate_while_loop(self, stmt: WhileLoop) -> None:
|
|
608
|
+
"""Generate while loop."""
|
|
609
|
+
condition = self.generate_expression(stmt.condition)
|
|
610
|
+
self.emit(f'while {condition}:')
|
|
611
|
+
|
|
612
|
+
self.indent_level += 1
|
|
613
|
+
for s in stmt.body:
|
|
614
|
+
self.generate_statement(s)
|
|
615
|
+
self.indent_level -= 1
|
|
616
|
+
|
|
617
|
+
def generate_return_statement(self, stmt: ReturnStatement) -> None:
|
|
618
|
+
"""Generate return statement."""
|
|
619
|
+
if stmt.expr:
|
|
620
|
+
self.emit(f'return {self.generate_expression(stmt.expr)}')
|
|
621
|
+
else:
|
|
622
|
+
self.emit('return')
|
|
623
|
+
|
|
624
|
+
# ========================================================================
|
|
625
|
+
# Expression generation
|
|
626
|
+
# ========================================================================
|
|
627
|
+
|
|
628
|
+
def generate_expression(self, expr: Expression) -> str:
|
|
629
|
+
"""Generate an expression."""
|
|
630
|
+
if isinstance(expr, BinaryOp):
|
|
631
|
+
return self.generate_binary_op(expr)
|
|
632
|
+
elif isinstance(expr, UnaryOp):
|
|
633
|
+
return self.generate_unary_op(expr)
|
|
634
|
+
elif isinstance(expr, TernaryOp):
|
|
635
|
+
return self.generate_ternary_op(expr)
|
|
636
|
+
elif isinstance(expr, FunctionCall):
|
|
637
|
+
return self.generate_function_call(expr)
|
|
638
|
+
elif isinstance(expr, MethodCall):
|
|
639
|
+
return self.generate_method_call(expr)
|
|
640
|
+
elif isinstance(expr, MemberAccess):
|
|
641
|
+
return self.generate_member_access(expr)
|
|
642
|
+
elif isinstance(expr, IndexAccess):
|
|
643
|
+
return self.generate_index_access(expr)
|
|
644
|
+
elif isinstance(expr, ArrayLiteral):
|
|
645
|
+
return self.generate_array_literal(expr)
|
|
646
|
+
elif isinstance(expr, Identifier):
|
|
647
|
+
return self.generate_identifier(expr)
|
|
648
|
+
elif isinstance(expr, NamedExpression):
|
|
649
|
+
return self.generate_named_expression(expr)
|
|
650
|
+
elif isinstance(expr, Literal):
|
|
651
|
+
return self.generate_literal(expr)
|
|
652
|
+
elif isinstance(expr, NaLiteral):
|
|
653
|
+
return 'na'
|
|
654
|
+
else:
|
|
655
|
+
# A comment in expression position is not Python; refuse (card #40)
|
|
656
|
+
raise CodeGenError(f'Unsupported expression type: {type(expr).__name__}',
|
|
657
|
+
line=self.refusal_line(expr))
|
|
658
|
+
|
|
659
|
+
def generate_identifier(self, expr: Identifier) -> str:
|
|
660
|
+
"""Generate identifier."""
|
|
661
|
+
from .pine_builtins import TA_VARIABLE_FUNCTIONS
|
|
662
|
+
|
|
663
|
+
name = self._esc(expr.name)
|
|
664
|
+
|
|
665
|
+
# Pine Script "variable-functions": used without () in Pine Script
|
|
666
|
+
# but implemented as functions in PyneCore (e.g., ta.vwap -> ta.vwap())
|
|
667
|
+
if name in TA_VARIABLE_FUNCTIONS:
|
|
668
|
+
# The table's value is the argument text; every entry is '' today
|
|
669
|
+
defaults = TA_VARIABLE_FUNCTIONS.get(name, '')
|
|
670
|
+
return f'{name}({defaults})'
|
|
671
|
+
|
|
672
|
+
return name
|
|
673
|
+
|
|
674
|
+
def generate_named_expression(self, expr: NamedExpression) -> str:
|
|
675
|
+
"""Generate `(name := value)`. Always parenthesised: a bare walrus is
|
|
676
|
+
illegal in most positions, and the parentheses keep a comparison in
|
|
677
|
+
`value` from chaining with an enclosing `==`."""
|
|
678
|
+
return f'({self._esc(expr.name)} := {self.generate_expression(expr.value)})'
|
|
679
|
+
|
|
680
|
+
def generate_binary_op(self, expr: BinaryOp) -> str:
|
|
681
|
+
"""Generate binary operation."""
|
|
682
|
+
if self.strict_boolean_operators:
|
|
683
|
+
strict = strict_operands(expr, self.user_function_names)
|
|
684
|
+
if strict:
|
|
685
|
+
return self._generate_strict_boolean(expr.op, strict)
|
|
686
|
+
left = self.generate_expression(expr.left)
|
|
687
|
+
right = self.generate_expression(expr.right)
|
|
688
|
+
|
|
689
|
+
if self._operand_needs_parentheses(expr.op, expr.left, is_right_operand=False):
|
|
690
|
+
left = f'({left})'
|
|
691
|
+
if self._operand_needs_parentheses(expr.op, expr.right, is_right_operand=True):
|
|
692
|
+
right = f'({right})'
|
|
693
|
+
|
|
694
|
+
return f'{left} {expr.op} {right}'
|
|
695
|
+
|
|
696
|
+
def _generate_strict_boolean(self, operator: str, operands: List[Expression]) -> str:
|
|
697
|
+
"""`all((a, b))` / `any((a, b))`: the tuple evaluates every operand, in order (card #65)."""
|
|
698
|
+
function = 'all' if operator == 'and' else 'any'
|
|
699
|
+
return f'{function}(({", ".join(self.generate_expression(operand) for operand in operands)}))'
|
|
700
|
+
|
|
701
|
+
@staticmethod
|
|
702
|
+
def _operand_needs_parentheses(parent_operator: str, operand: Expression, is_right_operand: bool) -> bool:
|
|
703
|
+
"""Whether an operand must be parenthesised to keep its Pine grouping.
|
|
704
|
+
|
|
705
|
+
The parser keeps no parenthesis node, so grouping exists only in the
|
|
706
|
+
tree shape and every operator-precedence parenthesis between binary
|
|
707
|
+
and unary operators is re-derived here (index and member access on a
|
|
708
|
+
grouped object are not: card #32).
|
|
709
|
+
"""
|
|
710
|
+
if isinstance(operand, UnaryOp):
|
|
711
|
+
# Pine `not` binds tightest; Python `not` binds looser than any
|
|
712
|
+
# comparison or arithmetic, so `q == not r` would not even parse.
|
|
713
|
+
return operand.op == 'not'
|
|
714
|
+
if not isinstance(operand, BinaryOp):
|
|
715
|
+
return False
|
|
716
|
+
if parent_operator in COMPARISON_OPERATORS and operand.op in COMPARISON_OPERATORS:
|
|
717
|
+
# Python chains `a < b == c` into `a < b and b == c`.
|
|
718
|
+
return True
|
|
719
|
+
parent_precedence = BINARY_OPERATOR_PRECEDENCE[parent_operator]
|
|
720
|
+
operand_precedence = BINARY_OPERATOR_PRECEDENCE[operand.op]
|
|
721
|
+
if operand_precedence != parent_precedence:
|
|
722
|
+
return operand_precedence < parent_precedence
|
|
723
|
+
# Same level: Pine is left-associative, so a right operand at this
|
|
724
|
+
# level was grouped explicitly (`a - (b - c)`, `a * (b % c)`).
|
|
725
|
+
return is_right_operand
|
|
726
|
+
|
|
727
|
+
def generate_unary_op(self, expr: UnaryOp) -> str:
|
|
728
|
+
"""Generate unary operation."""
|
|
729
|
+
operand = self.generate_expression(expr.operand)
|
|
730
|
+
|
|
731
|
+
# Convert Pine operators to Python
|
|
732
|
+
op_map = {
|
|
733
|
+
'not': 'not ',
|
|
734
|
+
}
|
|
735
|
+
op = op_map.get(expr.op, expr.op)
|
|
736
|
+
|
|
737
|
+
# Pine unary operators bind tighter than every binary operator, so a
|
|
738
|
+
# binary operand was grouped explicitly: -(a + b), not (a == b).
|
|
739
|
+
if isinstance(expr.operand, BinaryOp):
|
|
740
|
+
return f'{op}({operand})'
|
|
741
|
+
|
|
742
|
+
return f'{op}{operand}'
|
|
743
|
+
|
|
744
|
+
def generate_ternary_op(self, expr: TernaryOp) -> str:
|
|
745
|
+
"""Generate ternary operation (Python if/else expression)."""
|
|
746
|
+
condition = self.generate_expression(expr.condition)
|
|
747
|
+
true_expr = self.generate_expression(expr.true_expr)
|
|
748
|
+
false_expr = self.generate_expression(expr.false_expr) if expr.false_expr else 'None'
|
|
749
|
+
|
|
750
|
+
return f'({true_expr} if {condition} else {false_expr})'
|
|
751
|
+
|
|
752
|
+
def generate_function_call(self, call: FunctionCall) -> str:
|
|
753
|
+
"""Generate function call."""
|
|
754
|
+
# Generate function name
|
|
755
|
+
if isinstance(call.func, str):
|
|
756
|
+
func = call.func
|
|
757
|
+
elif isinstance(call.func, MemberAccess):
|
|
758
|
+
func = self.generate_member_access(call.func)
|
|
759
|
+
elif isinstance(call.func, NaLiteral):
|
|
760
|
+
# Special case: na(x) is a function call, not NA() literal
|
|
761
|
+
func = 'na'
|
|
762
|
+
# If NA() with no args, just return 'na' (for default params)
|
|
763
|
+
if not call.args and not call.kwargs:
|
|
764
|
+
return 'na'
|
|
765
|
+
else:
|
|
766
|
+
func = self.generate_expression(call.func)
|
|
767
|
+
|
|
768
|
+
# Generate arguments (positional - use single quotes for strings)
|
|
769
|
+
args = [self.generate_expression(arg) for arg in call.args]
|
|
770
|
+
|
|
771
|
+
# Generate keyword arguments (use double quotes for string literals)
|
|
772
|
+
kwargs = []
|
|
773
|
+
for key, value in call.kwargs.items():
|
|
774
|
+
# Special case: options parameter should use tuple () instead of list []
|
|
775
|
+
if key == 'options' and isinstance(value, ArrayLiteral):
|
|
776
|
+
elements = [self._generate_kwarg_value(e) for e in value.elements]
|
|
777
|
+
kwargs.append(f'{key}=({", ".join(elements)})')
|
|
778
|
+
else:
|
|
779
|
+
kwargs.append(f'{key}={self._generate_kwarg_value(value)}')
|
|
780
|
+
|
|
781
|
+
all_args = args + kwargs
|
|
782
|
+
return f'{func}({", ".join(all_args)})'
|
|
783
|
+
|
|
784
|
+
def _generate_kwarg_value(self, value: Expression) -> str:
|
|
785
|
+
"""Generate kwarg value with double quotes for strings (or the source's quotes)."""
|
|
786
|
+
if isinstance(value, Literal) and value.literal_type == 'string' and not self.keep_keyword_string_quotes:
|
|
787
|
+
return f'"{self._escape_string(value.value, chr(34))}"'
|
|
788
|
+
elif isinstance(value, Literal) and value.literal_type in ('int', 'float', 'bool'):
|
|
789
|
+
# For non-string literals, use normal generation
|
|
790
|
+
return self.generate_literal(value)
|
|
791
|
+
else:
|
|
792
|
+
return self.generate_expression(value)
|
|
793
|
+
|
|
794
|
+
def generate_method_call(self, call: MethodCall) -> str:
|
|
795
|
+
"""Generate method call. The transformer routes every builtin method to its
|
|
796
|
+
namespace (card #8), so what reaches here is a Python method call."""
|
|
797
|
+
from .ast_nodes import BinaryOp, UnaryOp, TernaryOp, IfExpression
|
|
798
|
+
|
|
799
|
+
obj = self.generate_expression(call.object)
|
|
800
|
+
args = [self.generate_expression(arg) for arg in call.args]
|
|
801
|
+
kwargs = [f'{key}={self.generate_expression(value)}' for key, value in call.kwargs.items()]
|
|
802
|
+
|
|
803
|
+
# FIX: Wrap complex expressions in parentheses to preserve precedence
|
|
804
|
+
# This ensures (expression).method() keeps correct semantics
|
|
805
|
+
if isinstance(call.object, (BinaryOp, UnaryOp, TernaryOp, IfExpression)):
|
|
806
|
+
obj = f'({obj})'
|
|
807
|
+
|
|
808
|
+
# Otherwise, keep as method call
|
|
809
|
+
all_args = args + kwargs
|
|
810
|
+
return f'{obj}.{call.method}({", ".join(all_args)})'
|
|
811
|
+
|
|
812
|
+
def generate_member_access(self, expr: MemberAccess) -> str:
|
|
813
|
+
"""Generate member access."""
|
|
814
|
+
from .pine_builtins import TA_VARIABLE_FUNCTIONS
|
|
815
|
+
|
|
816
|
+
if isinstance(expr.object, str):
|
|
817
|
+
obj = expr.object
|
|
818
|
+
elif isinstance(expr.object, Expression):
|
|
819
|
+
obj = self.generate_expression(expr.object)
|
|
820
|
+
else:
|
|
821
|
+
obj = str(expr.object)
|
|
822
|
+
|
|
823
|
+
result = f'{obj}.{expr.member}'
|
|
824
|
+
|
|
825
|
+
# Pine Script "variable-functions": used without () in Pine Script
|
|
826
|
+
# but implemented as functions in PyneCore (e.g., ta.vwap -> ta.vwap())
|
|
827
|
+
if result in TA_VARIABLE_FUNCTIONS:
|
|
828
|
+
defaults = TA_VARIABLE_FUNCTIONS.get(result, '')
|
|
829
|
+
result = f'{result}({defaults})'
|
|
830
|
+
|
|
831
|
+
return result
|
|
832
|
+
|
|
833
|
+
def generate_index_access(self, expr: IndexAccess) -> str:
|
|
834
|
+
"""Generate index access."""
|
|
835
|
+
obj = self.generate_expression(expr.object)
|
|
836
|
+
index = self.generate_expression(expr.index)
|
|
837
|
+
return f'{obj}[{index}]'
|
|
838
|
+
|
|
839
|
+
def generate_array_literal(self, expr: ArrayLiteral) -> str:
|
|
840
|
+
"""Generate a Pine tuple expression as a Python tuple.
|
|
841
|
+
|
|
842
|
+
Pine writes tuple literals with square brackets and has no bracket
|
|
843
|
+
syntax for arrays -- those come from ``array.new``/``array.from`` -- so
|
|
844
|
+
every bracket that reaches here is a tuple, never a collection literal.
|
|
845
|
+
Emitting a list instead makes the runtime take its array branch: it
|
|
846
|
+
answers ``request.seed``/``request.security`` with a single ``na``
|
|
847
|
+
rather than one ``na`` per element, and the caller's destructuring then
|
|
848
|
+
dies with "na is not iterable".
|
|
849
|
+
"""
|
|
850
|
+
elements = [self.generate_expression(e) for e in expr.elements]
|
|
851
|
+
if len(elements) == 1:
|
|
852
|
+
# A lone parenthesised value is not a tuple without the comma.
|
|
853
|
+
return f'({elements[0]},)'
|
|
854
|
+
return f'({", ".join(elements)})'
|
|
855
|
+
|
|
856
|
+
def _escape_string(self, value: str, quote: str) -> str:
|
|
857
|
+
"""Escape a string value for Python output."""
|
|
858
|
+
escaped = value.replace('\\', '\\\\')
|
|
859
|
+
escaped = escaped.replace('\n', '\\n')
|
|
860
|
+
escaped = escaped.replace('\t', '\\t')
|
|
861
|
+
escaped = escaped.replace('\r', '\\r')
|
|
862
|
+
escaped = escaped.replace(quote, f'\\{quote}')
|
|
863
|
+
return escaped
|
|
864
|
+
|
|
865
|
+
def generate_literal(self, expr: Literal) -> str:
|
|
866
|
+
"""Generate literal value."""
|
|
867
|
+
if expr.literal_type == 'string':
|
|
868
|
+
if expr.is_double_quoted:
|
|
869
|
+
return f'"{self._escape_string(expr.value, chr(34))}"'
|
|
870
|
+
else:
|
|
871
|
+
return f"'{self._escape_string(expr.value, chr(39))}'"
|
|
872
|
+
elif expr.literal_type == 'bool':
|
|
873
|
+
return 'True' if expr.value else 'False'
|
|
874
|
+
elif expr.literal_type == 'color':
|
|
875
|
+
if expr.is_double_quoted:
|
|
876
|
+
return f'"{self._escape_string(expr.value, chr(34))}"'
|
|
877
|
+
else:
|
|
878
|
+
return f"'{self._escape_string(expr.value, chr(39))}'"
|
|
879
|
+
else:
|
|
880
|
+
return str(expr.value)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def generate_code(script: PyneTransformedScript) -> str:
|
|
884
|
+
"""Convenience function to generate Python code from transformed script."""
|
|
885
|
+
codegen = CodeGenerator()
|
|
886
|
+
return codegen.generate(script)
|