opencode-pine2pyne 0.1.0__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.
- opencode_pine2pyne-0.1.0.dist-info/METADATA +123 -0
- opencode_pine2pyne-0.1.0.dist-info/RECORD +23 -0
- opencode_pine2pyne-0.1.0.dist-info/WHEEL +5 -0
- opencode_pine2pyne-0.1.0.dist-info/entry_points.txt +2 -0
- opencode_pine2pyne-0.1.0.dist-info/licenses/LICENSE +201 -0
- opencode_pine2pyne-0.1.0.dist-info/top_level.txt +1 -0
- pine2pyne/README.md +178 -0
- pine2pyne/TRANSPILER_BEST_PRACTICES.md +425 -0
- pine2pyne/TRANSPILER_USAGE.md +264 -0
- pine2pyne/__init__.py +86 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +352 -0
- pine2pyne/cli.py +212 -0
- pine2pyne/codegen.py +836 -0
- pine2pyne/errors.py +48 -0
- pine2pyne/import_resolver.py +407 -0
- pine2pyne/lexer.py +546 -0
- pine2pyne/parser.py +1510 -0
- pine2pyne/pine_builtins.py +464 -0
- pine2pyne/symbol_table.py +156 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2327 -0
- pine2pyne/type_inference.py +328 -0
pine2pyne/codegen.py
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code generator: Emit Python source code from transformed AST.
|
|
3
|
+
|
|
4
|
+
Generates PEP 8 compliant Python code compatible with PyneCore.
|
|
5
|
+
"""
|
|
6
|
+
from typing import List, Union
|
|
7
|
+
from .ast_nodes import *
|
|
8
|
+
from .transformer import PyneTransformedScript
|
|
9
|
+
from .errors import CodeGenError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CodeGenerator:
|
|
13
|
+
"""Generates Python source code from transformed AST."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, indent_size: int = 4):
|
|
16
|
+
self.indent_size = indent_size
|
|
17
|
+
self.indent_level = 0
|
|
18
|
+
self.output: List[str] = []
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _esc(name: str) -> str:
|
|
22
|
+
"""Escape Python reserved words used as identifiers."""
|
|
23
|
+
from .pine_builtins import sanitize_identifier
|
|
24
|
+
return sanitize_identifier(name)
|
|
25
|
+
|
|
26
|
+
def _get_all_type_hints(self, script) -> List[str]:
|
|
27
|
+
"""Extract all type hints from script for Any detection."""
|
|
28
|
+
hints = []
|
|
29
|
+
|
|
30
|
+
# Check main parameters
|
|
31
|
+
for param in script.main_params:
|
|
32
|
+
if param.type_hint:
|
|
33
|
+
hints.append(param.type_hint)
|
|
34
|
+
|
|
35
|
+
# Check main body statements
|
|
36
|
+
for stmt in script.main_body:
|
|
37
|
+
if hasattr(stmt, 'type_hint') and stmt.type_hint:
|
|
38
|
+
hints.append(stmt.type_hint)
|
|
39
|
+
|
|
40
|
+
# Check global vars
|
|
41
|
+
for var in script.global_vars:
|
|
42
|
+
if hasattr(var, 'type_hint') and var.type_hint:
|
|
43
|
+
hints.append(var.type_hint)
|
|
44
|
+
|
|
45
|
+
# Check helper functions
|
|
46
|
+
for func in script.helper_functions:
|
|
47
|
+
if func.return_type:
|
|
48
|
+
hints.append(func.return_type)
|
|
49
|
+
for param in func.params:
|
|
50
|
+
if param.type_hint:
|
|
51
|
+
hints.append(param.type_hint)
|
|
52
|
+
# Check function body (handle both list and single statement)
|
|
53
|
+
body = func.body if isinstance(func.body, list) else [func.body]
|
|
54
|
+
for stmt in body:
|
|
55
|
+
if hasattr(stmt, 'type_hint') and stmt.type_hint:
|
|
56
|
+
hints.append(stmt.type_hint)
|
|
57
|
+
|
|
58
|
+
return hints
|
|
59
|
+
|
|
60
|
+
def generate(self, script: PyneTransformedScript) -> str:
|
|
61
|
+
"""Generate complete Python source code."""
|
|
62
|
+
self.output = []
|
|
63
|
+
self._varname_to_slot = {name: (slot, deps) for slot, (name, deps) in script.cache_slots.items()}
|
|
64
|
+
self._num_cache_slots = script.num_cache_slots
|
|
65
|
+
|
|
66
|
+
# 1. @pyne magic docstring with attribution
|
|
67
|
+
self.emit('"""')
|
|
68
|
+
self.emit('@pyne')
|
|
69
|
+
self.emit('')
|
|
70
|
+
self.emit('This code was compiled by Pine2Pyne — the Pine Script to PyneCore\'s Python compiler.')
|
|
71
|
+
self.emit('"""')
|
|
72
|
+
self.emit('')
|
|
73
|
+
|
|
74
|
+
# 2. Import statements
|
|
75
|
+
# Check if Any is used in type hints
|
|
76
|
+
uses_any = any('[Any]' in stmt or ': Any' in stmt for stmt in self._get_all_type_hints(script))
|
|
77
|
+
|
|
78
|
+
if uses_any:
|
|
79
|
+
self.emit('from typing import Any')
|
|
80
|
+
|
|
81
|
+
for import_stmt in script.imports:
|
|
82
|
+
self.emit(import_stmt)
|
|
83
|
+
if script.imports or uses_any:
|
|
84
|
+
self.emit('')
|
|
85
|
+
|
|
86
|
+
# 2b. Variable cache support for pyne optimize
|
|
87
|
+
if self._num_cache_slots > 0:
|
|
88
|
+
self.emit('try:')
|
|
89
|
+
self.emit(' from pynecore.core import _var_cache as _vcm')
|
|
90
|
+
self.emit('except ImportError:')
|
|
91
|
+
self.emit(' _vcm = None')
|
|
92
|
+
self.emit('')
|
|
93
|
+
# Emit __var_deps__ dict
|
|
94
|
+
deps_parts = []
|
|
95
|
+
for slot in sorted(script.cache_slots):
|
|
96
|
+
_, deps = script.cache_slots[slot]
|
|
97
|
+
if deps:
|
|
98
|
+
items = ', '.join(f"'{d}'" for d in sorted(deps))
|
|
99
|
+
deps_parts.append(f'{slot}: frozenset({{{items}}})')
|
|
100
|
+
else:
|
|
101
|
+
deps_parts.append(f'{slot}: frozenset()')
|
|
102
|
+
self.emit(f'__var_deps__ = {{{", ".join(deps_parts)}}}')
|
|
103
|
+
self.emit(f'__num_cache_slots__ = {self._num_cache_slots}')
|
|
104
|
+
self.emit('')
|
|
105
|
+
|
|
106
|
+
# 3. Type declarations (custom types with @udt)
|
|
107
|
+
for type_decl in script.type_declarations:
|
|
108
|
+
self.generate_type_declaration(type_decl)
|
|
109
|
+
self.emit('')
|
|
110
|
+
|
|
111
|
+
# 3b. Enum declarations
|
|
112
|
+
for enum_decl in script.enum_declarations:
|
|
113
|
+
self.generate_enum_declaration(enum_decl)
|
|
114
|
+
self.emit('')
|
|
115
|
+
|
|
116
|
+
# 3c. Module-level constants (e.g., const string group labels, pre-main literals)
|
|
117
|
+
# These must be at module level because they are referenced by main()
|
|
118
|
+
# parameter defaults which Python evaluates at function definition time.
|
|
119
|
+
if script.module_constants:
|
|
120
|
+
for const_stmt in script.module_constants:
|
|
121
|
+
self.generate_statement(const_stmt)
|
|
122
|
+
self.emit('')
|
|
123
|
+
|
|
124
|
+
# 4. Main function with decorator (helper functions now inside main)
|
|
125
|
+
if script.main_decorator:
|
|
126
|
+
decorator = self.generate_decorator(script.main_decorator)
|
|
127
|
+
self.emit(decorator)
|
|
128
|
+
|
|
129
|
+
# Generate main function signature
|
|
130
|
+
self.emit('def main(')
|
|
131
|
+
self.indent_level += 1
|
|
132
|
+
|
|
133
|
+
# Generate parameters
|
|
134
|
+
if script.main_params:
|
|
135
|
+
for i, param in enumerate(script.main_params):
|
|
136
|
+
param_str = self.generate_parameter(param)
|
|
137
|
+
if i < len(script.main_params) - 1:
|
|
138
|
+
param_str += ','
|
|
139
|
+
self.emit(param_str)
|
|
140
|
+
else:
|
|
141
|
+
# No parameters - but Python requires at least empty ()
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
self.indent_level -= 1
|
|
145
|
+
|
|
146
|
+
# Close parameter list and add colon
|
|
147
|
+
if script.main_params:
|
|
148
|
+
self.emit('):')
|
|
149
|
+
else:
|
|
150
|
+
self.output[-1] += '):' # Add closing paren and colon to def line
|
|
151
|
+
|
|
152
|
+
self.indent_level += 1
|
|
153
|
+
|
|
154
|
+
# Emit variable cache locals at top of main body
|
|
155
|
+
if self._num_cache_slots > 0:
|
|
156
|
+
self.emit('_vc = _vcm._data if _vcm else None')
|
|
157
|
+
self.emit('_vb = _vcm._build if _vcm else None')
|
|
158
|
+
|
|
159
|
+
# Generate global var declarations (Persistent)
|
|
160
|
+
for var_decl in script.global_vars:
|
|
161
|
+
self.generate_statement(var_decl)
|
|
162
|
+
|
|
163
|
+
# Generate main body with blank lines between statements
|
|
164
|
+
# Rule: add blank line after function defs and between different statement kinds
|
|
165
|
+
prev_kind = None # 'persistent', 'func', 'body'
|
|
166
|
+
prev_source_line = 0
|
|
167
|
+
for i, stmt in enumerate(script.main_body):
|
|
168
|
+
source_line = getattr(stmt, 'line', 0)
|
|
169
|
+
|
|
170
|
+
# Determine statement kind
|
|
171
|
+
if isinstance(stmt, Assignment) and isinstance(stmt.type_hint, str) and 'Persistent' in stmt.type_hint:
|
|
172
|
+
kind = 'persistent'
|
|
173
|
+
elif isinstance(stmt, FuncDecl):
|
|
174
|
+
kind = 'func'
|
|
175
|
+
else:
|
|
176
|
+
kind = 'body'
|
|
177
|
+
|
|
178
|
+
# Add blank lines based on context
|
|
179
|
+
if i > 0:
|
|
180
|
+
gap = source_line - prev_source_line if source_line > 0 and prev_source_line > 0 else 0
|
|
181
|
+
|
|
182
|
+
if prev_kind == 'func':
|
|
183
|
+
# Always blank line after function definition
|
|
184
|
+
self.emit('')
|
|
185
|
+
elif prev_kind != kind:
|
|
186
|
+
# Transition between statement types
|
|
187
|
+
if gap > 5:
|
|
188
|
+
# Large gap (section change) - add extra blank lines
|
|
189
|
+
blanks = min((gap - 1) // 10 + 1, 3)
|
|
190
|
+
for _ in range(blanks):
|
|
191
|
+
self.emit('')
|
|
192
|
+
else:
|
|
193
|
+
self.emit('')
|
|
194
|
+
elif kind == 'persistent' and gap >= 3:
|
|
195
|
+
# Gap between Persistent groups (e.g., maps → arrays)
|
|
196
|
+
self.emit('')
|
|
197
|
+
elif kind == 'body' and gap >= 3:
|
|
198
|
+
# Gap between body statements (comment/blank line in source)
|
|
199
|
+
self.emit('')
|
|
200
|
+
|
|
201
|
+
self.generate_statement(stmt)
|
|
202
|
+
prev_kind = kind
|
|
203
|
+
prev_source_line = source_line
|
|
204
|
+
|
|
205
|
+
self.indent_level -= 1
|
|
206
|
+
|
|
207
|
+
return '\n'.join(self.output)
|
|
208
|
+
|
|
209
|
+
# ========================================================================
|
|
210
|
+
# Emission helpers
|
|
211
|
+
# ========================================================================
|
|
212
|
+
|
|
213
|
+
def emit(self, line: str = '') -> None:
|
|
214
|
+
"""Emit a line of code with current indentation."""
|
|
215
|
+
if line:
|
|
216
|
+
indent = ' ' * (self.indent_level * self.indent_size)
|
|
217
|
+
self.output.append(indent + line)
|
|
218
|
+
else:
|
|
219
|
+
self.output.append('')
|
|
220
|
+
|
|
221
|
+
def emit_inline(self, text: str) -> None:
|
|
222
|
+
"""Emit text without newline (append to last line)."""
|
|
223
|
+
if self.output:
|
|
224
|
+
self.output[-1] += text
|
|
225
|
+
else:
|
|
226
|
+
self.output.append(text)
|
|
227
|
+
|
|
228
|
+
# ========================================================================
|
|
229
|
+
# Top-level code generation
|
|
230
|
+
# ========================================================================
|
|
231
|
+
|
|
232
|
+
def generate_decorator(self, decl: Union[IndicatorDecl, StrategyDecl]) -> str:
|
|
233
|
+
"""Generate @script.indicator or @script.strategy decorator."""
|
|
234
|
+
# Escape single quotes in title to avoid breaking the string literal
|
|
235
|
+
title = decl.title.replace("'", "\\'") if decl.title else ''
|
|
236
|
+
if isinstance(decl, IndicatorDecl):
|
|
237
|
+
decorator = f"@script.indicator('{title}'"
|
|
238
|
+
else:
|
|
239
|
+
decorator = f"@script.strategy('{title}'"
|
|
240
|
+
|
|
241
|
+
# Add kwargs
|
|
242
|
+
if decl.kwargs:
|
|
243
|
+
for key, value in decl.kwargs.items():
|
|
244
|
+
decorator += f', {key}={self.generate_decorator_value(value)}'
|
|
245
|
+
|
|
246
|
+
decorator += ')'
|
|
247
|
+
return decorator
|
|
248
|
+
|
|
249
|
+
def generate_decorator_value(self, value: Any) -> str:
|
|
250
|
+
"""Generate decorator kwarg value."""
|
|
251
|
+
if isinstance(value, bool):
|
|
252
|
+
return 'True' if value else 'False'
|
|
253
|
+
elif isinstance(value, (int, float)):
|
|
254
|
+
return str(value)
|
|
255
|
+
elif isinstance(value, str):
|
|
256
|
+
return f'"{value}"'
|
|
257
|
+
elif isinstance(value, Literal):
|
|
258
|
+
return self.generate_literal(value)
|
|
259
|
+
elif isinstance(value, MemberAccess):
|
|
260
|
+
return self.generate_member_access(value)
|
|
261
|
+
elif isinstance(value, Identifier):
|
|
262
|
+
return value.name
|
|
263
|
+
elif isinstance(value, Expression):
|
|
264
|
+
# Handle any other expression type
|
|
265
|
+
return self.generate_expression(value)
|
|
266
|
+
else:
|
|
267
|
+
return str(value)
|
|
268
|
+
|
|
269
|
+
def generate_parameter(self, param: Parameter) -> str:
|
|
270
|
+
"""Generate function parameter."""
|
|
271
|
+
param_str = self._esc(param.name)
|
|
272
|
+
|
|
273
|
+
# Add type hint
|
|
274
|
+
if param.type_hint:
|
|
275
|
+
param_str += f': {param.type_hint}'
|
|
276
|
+
|
|
277
|
+
# Add default value
|
|
278
|
+
if param.default:
|
|
279
|
+
default_str = self.generate_expression(param.default)
|
|
280
|
+
# Fix NA() to na for function default parameters
|
|
281
|
+
if default_str == 'NA()':
|
|
282
|
+
default_str = 'na'
|
|
283
|
+
# PEP 8: spaces around = in defaults when type annotation present
|
|
284
|
+
if param.type_hint:
|
|
285
|
+
param_str += f' = {default_str}'
|
|
286
|
+
else:
|
|
287
|
+
param_str += f'={default_str}'
|
|
288
|
+
|
|
289
|
+
return param_str
|
|
290
|
+
|
|
291
|
+
# ========================================================================
|
|
292
|
+
# Type declaration generation
|
|
293
|
+
# ========================================================================
|
|
294
|
+
|
|
295
|
+
def generate_type_declaration(self, type_decl: TypeDecl) -> None:
|
|
296
|
+
"""Generate custom type definition with @udt decorator."""
|
|
297
|
+
from .transformer import convert_pine_generic_to_python
|
|
298
|
+
from .pine_builtins import get_type_name
|
|
299
|
+
|
|
300
|
+
# Emit @udt decorator
|
|
301
|
+
self.emit('@udt')
|
|
302
|
+
self.emit(f'class {self._esc(type_decl.name)}:')
|
|
303
|
+
self.indent_level += 1
|
|
304
|
+
|
|
305
|
+
# Generate fields
|
|
306
|
+
for field_name, field_type, default_value in type_decl.fields:
|
|
307
|
+
# Convert Pine type to Python type (handles generics like array<T> → list[T])
|
|
308
|
+
py_type = convert_pine_generic_to_python(field_type)
|
|
309
|
+
fname = self._esc(field_name)
|
|
310
|
+
|
|
311
|
+
# For NA() calls, use base type only (NA(list) not NA(list[X]))
|
|
312
|
+
na_type = py_type.split('[')[0] if '[' in py_type else py_type
|
|
313
|
+
|
|
314
|
+
# Generate field with type annotation and default
|
|
315
|
+
if default_value:
|
|
316
|
+
if isinstance(default_value, NaLiteral):
|
|
317
|
+
# na default becomes NA(type)
|
|
318
|
+
self.emit(f'{fname}: {py_type} = NA({na_type})')
|
|
319
|
+
else:
|
|
320
|
+
default_str = self.generate_expression(default_value)
|
|
321
|
+
self.emit(f'{fname}: {py_type} = {default_str}')
|
|
322
|
+
else:
|
|
323
|
+
# No default - use type's zero value or na()
|
|
324
|
+
if py_type == 'int':
|
|
325
|
+
self.emit(f'{fname}: {py_type} = 0')
|
|
326
|
+
elif py_type == 'float':
|
|
327
|
+
self.emit(f'{fname}: {py_type} = 0.0')
|
|
328
|
+
elif py_type == 'str':
|
|
329
|
+
# str type should use NA(str) not empty string
|
|
330
|
+
self.emit(f'{fname}: {py_type} = NA({py_type})')
|
|
331
|
+
elif py_type == 'bool':
|
|
332
|
+
self.emit(f'{fname}: {py_type} = False')
|
|
333
|
+
else:
|
|
334
|
+
self.emit(f'{fname}: {py_type} = NA({na_type})')
|
|
335
|
+
|
|
336
|
+
self.indent_level -= 1
|
|
337
|
+
|
|
338
|
+
def generate_enum_declaration(self, enum_decl: EnumDecl) -> None:
|
|
339
|
+
"""Generate enum as a plain class with string constants."""
|
|
340
|
+
self.emit(f'class {self._esc(enum_decl.name)}:')
|
|
341
|
+
self.indent_level += 1
|
|
342
|
+
|
|
343
|
+
for member in enum_decl.members:
|
|
344
|
+
if isinstance(member, tuple):
|
|
345
|
+
name, value = member
|
|
346
|
+
value_str = self.generate_expression(value)
|
|
347
|
+
self.emit(f'{self._esc(name)} = {value_str}')
|
|
348
|
+
else:
|
|
349
|
+
# No explicit value — use member name as string value
|
|
350
|
+
self.emit(f'{self._esc(member)} = "{member}"')
|
|
351
|
+
|
|
352
|
+
self.indent_level -= 1
|
|
353
|
+
|
|
354
|
+
# ========================================================================
|
|
355
|
+
# Function generation
|
|
356
|
+
# ========================================================================
|
|
357
|
+
|
|
358
|
+
def generate_function(self, func: FuncDecl) -> None:
|
|
359
|
+
"""Generate function definition."""
|
|
360
|
+
# Emit @method decorator if this is a method
|
|
361
|
+
if hasattr(func, 'is_method') and func.is_method:
|
|
362
|
+
self.emit('@method')
|
|
363
|
+
|
|
364
|
+
# Function signature
|
|
365
|
+
params = ', '.join(self.generate_parameter(p) for p in func.params)
|
|
366
|
+
self.emit(f'def {self._esc(func.name)}({params}):')
|
|
367
|
+
|
|
368
|
+
self.indent_level += 1
|
|
369
|
+
|
|
370
|
+
# Track if we emitted any body content
|
|
371
|
+
body_was_empty = False
|
|
372
|
+
|
|
373
|
+
# Function body
|
|
374
|
+
if isinstance(func.body, (list, tuple)):
|
|
375
|
+
for i, stmt in enumerate(func.body):
|
|
376
|
+
is_last = (i == len(func.body) - 1)
|
|
377
|
+
# Last expression in body is the return value
|
|
378
|
+
if is_last and isinstance(stmt, Expression):
|
|
379
|
+
self.emit(f'return {self.generate_expression(stmt)}')
|
|
380
|
+
else:
|
|
381
|
+
self.generate_statement(stmt)
|
|
382
|
+
# If body list was empty or all statements were None, add pass
|
|
383
|
+
if not func.body or all(s is None for s in func.body):
|
|
384
|
+
self.emit('pass')
|
|
385
|
+
body_was_empty = True
|
|
386
|
+
elif isinstance(func.body, Expression):
|
|
387
|
+
# Single expression - add return
|
|
388
|
+
self.emit(f'return {self.generate_expression(func.body)}')
|
|
389
|
+
elif isinstance(func.body, ReturnStatement):
|
|
390
|
+
self.generate_statement(func.body)
|
|
391
|
+
else:
|
|
392
|
+
# Empty or None body - add pass
|
|
393
|
+
self.emit('pass')
|
|
394
|
+
body_was_empty = True
|
|
395
|
+
|
|
396
|
+
self.indent_level -= 1
|
|
397
|
+
|
|
398
|
+
# ========================================================================
|
|
399
|
+
# Statement generation
|
|
400
|
+
# ========================================================================
|
|
401
|
+
|
|
402
|
+
def generate_statement(self, stmt: Statement) -> None:
|
|
403
|
+
"""Generate a statement."""
|
|
404
|
+
# Flatten lists of statements (from transforms that expand one stmt into multiple)
|
|
405
|
+
if isinstance(stmt, list):
|
|
406
|
+
for s in stmt:
|
|
407
|
+
self.generate_statement(s)
|
|
408
|
+
return
|
|
409
|
+
if isinstance(stmt, FuncDecl):
|
|
410
|
+
# Functions can now appear inside main()
|
|
411
|
+
self.generate_function(stmt)
|
|
412
|
+
self.emit('') # Add blank line after function
|
|
413
|
+
elif isinstance(stmt, Assignment):
|
|
414
|
+
self.generate_assignment(stmt)
|
|
415
|
+
elif isinstance(stmt, IfStatement):
|
|
416
|
+
self.generate_if_statement(stmt)
|
|
417
|
+
elif isinstance(stmt, ForLoop):
|
|
418
|
+
self.generate_for_loop(stmt)
|
|
419
|
+
elif isinstance(stmt, ForInLoop):
|
|
420
|
+
self.generate_for_in_loop(stmt)
|
|
421
|
+
elif isinstance(stmt, WhileLoop):
|
|
422
|
+
self.generate_while_loop(stmt)
|
|
423
|
+
elif isinstance(stmt, BreakStatement):
|
|
424
|
+
self.emit('break')
|
|
425
|
+
elif isinstance(stmt, ContinueStatement):
|
|
426
|
+
self.emit('continue')
|
|
427
|
+
elif isinstance(stmt, ReturnStatement):
|
|
428
|
+
self.generate_return_statement(stmt)
|
|
429
|
+
elif isinstance(stmt, ExpressionStatement):
|
|
430
|
+
self.emit(self.generate_expression(stmt.expr))
|
|
431
|
+
elif isinstance(stmt, RawCode):
|
|
432
|
+
for line in stmt.code.split('\n'):
|
|
433
|
+
self.emit(line)
|
|
434
|
+
else:
|
|
435
|
+
self.emit(f'# TODO: Unsupported statement type: {type(stmt).__name__}')
|
|
436
|
+
|
|
437
|
+
def generate_assignment(self, stmt: Assignment) -> None:
|
|
438
|
+
"""Generate assignment statement."""
|
|
439
|
+
from .ast_nodes import BinaryOp, Identifier, MemberAccess, TupleDestructure
|
|
440
|
+
|
|
441
|
+
# Handle tuple destructuring: [a, b, c] = expr
|
|
442
|
+
if isinstance(stmt.target, TupleDestructure):
|
|
443
|
+
names = ', '.join(self._esc(n) for n in stmt.target.names)
|
|
444
|
+
value = self.generate_expression(stmt.value)
|
|
445
|
+
type_hint = f': {stmt.type_hint}' if stmt.type_hint else ''
|
|
446
|
+
self.emit(f'{names}{type_hint} = {value}')
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
# Check for variable cache slot (pyne optimize support)
|
|
450
|
+
if isinstance(stmt.target, str) and stmt.target in self._varname_to_slot:
|
|
451
|
+
slot, _ = self._varname_to_slot[stmt.target]
|
|
452
|
+
target = self._esc(stmt.target)
|
|
453
|
+
value_expr = self.generate_expression(stmt.value)
|
|
454
|
+
cache_expr = f'_vc[{slot}][int(bar_index)] if _vc is not None and _vc[{slot}] is not None else {value_expr}'
|
|
455
|
+
if stmt.type_hint:
|
|
456
|
+
self.emit(f'{target}: {stmt.type_hint} = {cache_expr}')
|
|
457
|
+
else:
|
|
458
|
+
self.emit(f'{target} = {cache_expr}')
|
|
459
|
+
self.emit(f'if _vb is not None and _vb[{slot}] is not None: _vb[{slot}].append({target})')
|
|
460
|
+
return
|
|
461
|
+
|
|
462
|
+
target = self._esc(stmt.target if isinstance(stmt.target, str) else str(stmt.target))
|
|
463
|
+
|
|
464
|
+
# Check if this is a compound assignment (x = x + y) that should be (x += y)
|
|
465
|
+
if isinstance(stmt.value, BinaryOp) and not stmt.type_hint:
|
|
466
|
+
left_matches = False
|
|
467
|
+
|
|
468
|
+
# Check if the left side of the BinaryOp matches the target
|
|
469
|
+
if isinstance(stmt.value.left, Identifier):
|
|
470
|
+
left_matches = (stmt.value.left.name == target)
|
|
471
|
+
elif isinstance(stmt.value.left, MemberAccess):
|
|
472
|
+
# For member access like this.wins, reconstruct the full name
|
|
473
|
+
left_name = f"{stmt.value.left.object}.{stmt.value.left.member}"
|
|
474
|
+
left_matches = (left_name == target)
|
|
475
|
+
|
|
476
|
+
# If it's a compound assignment pattern, generate +=, -=, etc.
|
|
477
|
+
if left_matches and stmt.value.op in ['+', '-', '*', '/', '%']:
|
|
478
|
+
compound_op = f'{stmt.value.op}='
|
|
479
|
+
right_value = self.generate_expression(stmt.value.right)
|
|
480
|
+
self.emit(f'{target} {compound_op} {right_value}')
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
# Regular assignment
|
|
484
|
+
value = self.generate_expression(stmt.value)
|
|
485
|
+
|
|
486
|
+
# Wrap string literals assigned to Color type as Color('...')
|
|
487
|
+
if stmt.type_hint == 'Color' and value.startswith("'") and value.endswith("'"):
|
|
488
|
+
value = f'Color({value})'
|
|
489
|
+
|
|
490
|
+
# Add type hint if present
|
|
491
|
+
if stmt.type_hint:
|
|
492
|
+
self.emit(f'{target}: {stmt.type_hint} = {value}')
|
|
493
|
+
else:
|
|
494
|
+
self.emit(f'{target} = {value}')
|
|
495
|
+
|
|
496
|
+
def generate_if_statement(self, stmt: IfStatement) -> None:
|
|
497
|
+
"""Generate if statement."""
|
|
498
|
+
condition = self.generate_expression(stmt.condition)
|
|
499
|
+
self.emit(f'if {condition}:')
|
|
500
|
+
|
|
501
|
+
self.indent_level += 1
|
|
502
|
+
for s in stmt.body:
|
|
503
|
+
self.generate_statement(s)
|
|
504
|
+
self.indent_level -= 1
|
|
505
|
+
|
|
506
|
+
# Generate elif clauses
|
|
507
|
+
for elif_cond, elif_body in stmt.elseifs:
|
|
508
|
+
elif_condition = self.generate_expression(elif_cond)
|
|
509
|
+
self.emit(f'elif {elif_condition}:')
|
|
510
|
+
self.indent_level += 1
|
|
511
|
+
for s in elif_body:
|
|
512
|
+
self.generate_statement(s)
|
|
513
|
+
self.indent_level -= 1
|
|
514
|
+
|
|
515
|
+
# Generate else clause
|
|
516
|
+
if stmt.else_body:
|
|
517
|
+
self.emit('else:')
|
|
518
|
+
self.indent_level += 1
|
|
519
|
+
for s in stmt.else_body:
|
|
520
|
+
self.generate_statement(s)
|
|
521
|
+
self.indent_level -= 1
|
|
522
|
+
|
|
523
|
+
def generate_for_loop(self, stmt: ForLoop) -> None:
|
|
524
|
+
"""Generate for loop with pine_range()."""
|
|
525
|
+
# Convert Pine 'for i = from to to_val [by step]' to Python pine_range()
|
|
526
|
+
from_val = self.generate_expression(stmt.from_val)
|
|
527
|
+
to_val = self.generate_expression(stmt.to_val)
|
|
528
|
+
|
|
529
|
+
# Pine's 'to' is inclusive, pine_range handles this correctly
|
|
530
|
+
# No need to add 1 like we did with range()
|
|
531
|
+
var = self._esc(stmt.var)
|
|
532
|
+
if stmt.step:
|
|
533
|
+
step = self.generate_expression(stmt.step)
|
|
534
|
+
self.emit(f'for {var} in pine_range({from_val}, {to_val}, {step}):')
|
|
535
|
+
else:
|
|
536
|
+
self.emit(f'for {var} in pine_range({from_val}, {to_val}):')
|
|
537
|
+
|
|
538
|
+
self.indent_level += 1
|
|
539
|
+
for s in stmt.body:
|
|
540
|
+
self.generate_statement(s)
|
|
541
|
+
self.indent_level -= 1
|
|
542
|
+
|
|
543
|
+
def generate_for_in_loop(self, stmt: ForInLoop) -> None:
|
|
544
|
+
"""Generate for-in loop."""
|
|
545
|
+
vars_str = ', '.join(self._esc(v) for v in stmt.vars)
|
|
546
|
+
iterable = self.generate_expression(stmt.iterable)
|
|
547
|
+
|
|
548
|
+
# If multiple vars, it's tuple unpacking
|
|
549
|
+
if len(stmt.vars) > 1:
|
|
550
|
+
self.emit(f'for {vars_str} in enumerate({iterable}):')
|
|
551
|
+
else:
|
|
552
|
+
self.emit(f'for {vars_str} in {iterable}:')
|
|
553
|
+
|
|
554
|
+
self.indent_level += 1
|
|
555
|
+
for s in stmt.body:
|
|
556
|
+
self.generate_statement(s)
|
|
557
|
+
self.indent_level -= 1
|
|
558
|
+
|
|
559
|
+
def generate_while_loop(self, stmt: WhileLoop) -> None:
|
|
560
|
+
"""Generate while loop."""
|
|
561
|
+
condition = self.generate_expression(stmt.condition)
|
|
562
|
+
self.emit(f'while {condition}:')
|
|
563
|
+
|
|
564
|
+
self.indent_level += 1
|
|
565
|
+
for s in stmt.body:
|
|
566
|
+
self.generate_statement(s)
|
|
567
|
+
self.indent_level -= 1
|
|
568
|
+
|
|
569
|
+
def generate_return_statement(self, stmt: ReturnStatement) -> None:
|
|
570
|
+
"""Generate return statement."""
|
|
571
|
+
if stmt.expr:
|
|
572
|
+
self.emit(f'return {self.generate_expression(stmt.expr)}')
|
|
573
|
+
else:
|
|
574
|
+
self.emit('return')
|
|
575
|
+
|
|
576
|
+
# ========================================================================
|
|
577
|
+
# Expression generation
|
|
578
|
+
# ========================================================================
|
|
579
|
+
|
|
580
|
+
def generate_expression(self, expr: Expression) -> str:
|
|
581
|
+
"""Generate an expression."""
|
|
582
|
+
if isinstance(expr, BinaryOp):
|
|
583
|
+
return self.generate_binary_op(expr)
|
|
584
|
+
elif isinstance(expr, UnaryOp):
|
|
585
|
+
return self.generate_unary_op(expr)
|
|
586
|
+
elif isinstance(expr, TernaryOp):
|
|
587
|
+
return self.generate_ternary_op(expr)
|
|
588
|
+
elif isinstance(expr, FunctionCall):
|
|
589
|
+
return self.generate_function_call(expr)
|
|
590
|
+
elif isinstance(expr, MethodCall):
|
|
591
|
+
return self.generate_method_call(expr)
|
|
592
|
+
elif isinstance(expr, MemberAccess):
|
|
593
|
+
return self.generate_member_access(expr)
|
|
594
|
+
elif isinstance(expr, IndexAccess):
|
|
595
|
+
return self.generate_index_access(expr)
|
|
596
|
+
elif isinstance(expr, ArrayLiteral):
|
|
597
|
+
return self.generate_array_literal(expr)
|
|
598
|
+
elif isinstance(expr, Identifier):
|
|
599
|
+
return self.generate_identifier(expr)
|
|
600
|
+
elif isinstance(expr, Literal):
|
|
601
|
+
return self.generate_literal(expr)
|
|
602
|
+
elif isinstance(expr, NaLiteral):
|
|
603
|
+
return 'na'
|
|
604
|
+
else:
|
|
605
|
+
return f'# TODO: {type(expr).__name__}'
|
|
606
|
+
|
|
607
|
+
def generate_identifier(self, expr: Identifier) -> str:
|
|
608
|
+
"""Generate identifier."""
|
|
609
|
+
from .pine_builtins import TA_VARIABLE_FUNCTIONS
|
|
610
|
+
|
|
611
|
+
name = self._esc(expr.name)
|
|
612
|
+
|
|
613
|
+
# Pine Script "variable-functions": used without () in Pine Script
|
|
614
|
+
# but implemented as functions in PyneCore (e.g., ta.vwap -> ta.vwap())
|
|
615
|
+
if name in TA_VARIABLE_FUNCTIONS:
|
|
616
|
+
# Some functions need default args (e.g., ta.vwap defaults to hlc3)
|
|
617
|
+
defaults = TA_VARIABLE_FUNCTIONS.get(name, '')
|
|
618
|
+
return f'{name}({defaults})'
|
|
619
|
+
|
|
620
|
+
return name
|
|
621
|
+
|
|
622
|
+
def generate_binary_op(self, expr: BinaryOp) -> str:
|
|
623
|
+
"""Generate binary operation."""
|
|
624
|
+
left = self.generate_expression(expr.left)
|
|
625
|
+
right = self.generate_expression(expr.right)
|
|
626
|
+
|
|
627
|
+
# Convert Pine operators to Python
|
|
628
|
+
op_map = {
|
|
629
|
+
'and': 'and',
|
|
630
|
+
'or': 'or',
|
|
631
|
+
'not': 'not',
|
|
632
|
+
}
|
|
633
|
+
op = op_map.get(expr.op, expr.op)
|
|
634
|
+
|
|
635
|
+
# Add parentheses based on operator precedence rules
|
|
636
|
+
# Precedence: * / % (high) > + - (low)
|
|
637
|
+
LOWER_THAN_MUL = ('+', '-')
|
|
638
|
+
|
|
639
|
+
# Multiplication: wrap lower-precedence operands
|
|
640
|
+
if expr.op == '*':
|
|
641
|
+
if isinstance(expr.left, BinaryOp) and expr.left.op in LOWER_THAN_MUL:
|
|
642
|
+
left = f'({left})'
|
|
643
|
+
if isinstance(expr.right, BinaryOp) and expr.right.op in ('+', '-', '/'):
|
|
644
|
+
right = f'({right})'
|
|
645
|
+
return f'{left} * {right}'
|
|
646
|
+
|
|
647
|
+
# Division: wrap lower-precedence left operand, and any right operand
|
|
648
|
+
if expr.op == '/':
|
|
649
|
+
if isinstance(expr.left, BinaryOp) and expr.left.op in LOWER_THAN_MUL:
|
|
650
|
+
left = f'({left})'
|
|
651
|
+
if isinstance(expr.right, BinaryOp) and expr.right.op in ('+', '-', '/', '*'):
|
|
652
|
+
right = f'({right})'
|
|
653
|
+
return f'{left} / {right}'
|
|
654
|
+
|
|
655
|
+
# Modulo: wrap lower-precedence operands
|
|
656
|
+
if expr.op == '%':
|
|
657
|
+
if isinstance(expr.left, BinaryOp) and expr.left.op in LOWER_THAN_MUL:
|
|
658
|
+
left = f'({left})'
|
|
659
|
+
if isinstance(expr.right, BinaryOp) and expr.right.op in ('+', '-', '/', '*', '%'):
|
|
660
|
+
right = f'({right})'
|
|
661
|
+
return f'{left} % {right}'
|
|
662
|
+
|
|
663
|
+
# Operator precedence handling for logical operators
|
|
664
|
+
if expr.op in ('and', 'or'):
|
|
665
|
+
# Wrap 'not' in parens when used as operand of 'and'/'or'
|
|
666
|
+
if isinstance(expr.right, UnaryOp) and expr.right.op == 'not':
|
|
667
|
+
right = f'({right})'
|
|
668
|
+
if isinstance(expr.left, UnaryOp) and expr.left.op == 'not':
|
|
669
|
+
left = f'({left})'
|
|
670
|
+
# Wrap 'or' in parens when used as operand of 'and' (lower precedence)
|
|
671
|
+
if expr.op == 'and':
|
|
672
|
+
if isinstance(expr.left, BinaryOp) and expr.left.op == 'or':
|
|
673
|
+
left = f'({left})'
|
|
674
|
+
if isinstance(expr.right, BinaryOp) and expr.right.op == 'or':
|
|
675
|
+
right = f'({right})'
|
|
676
|
+
|
|
677
|
+
# Don't wrap in parentheses for other ops - Python operator precedence handles this
|
|
678
|
+
return f'{left} {op} {right}'
|
|
679
|
+
|
|
680
|
+
def generate_unary_op(self, expr: UnaryOp) -> str:
|
|
681
|
+
"""Generate unary operation."""
|
|
682
|
+
operand = self.generate_expression(expr.operand)
|
|
683
|
+
|
|
684
|
+
# Convert Pine operators to Python
|
|
685
|
+
op_map = {
|
|
686
|
+
'not': 'not ',
|
|
687
|
+
}
|
|
688
|
+
op = op_map.get(expr.op, expr.op)
|
|
689
|
+
|
|
690
|
+
# In Python, 'not' has lower precedence than comparison operators but
|
|
691
|
+
# higher than 'and'/'or'. Pine's not(A or B) must keep parentheses,
|
|
692
|
+
# otherwise Python parses 'not A or B' as '(not A) or B'.
|
|
693
|
+
if expr.op == 'not' and isinstance(expr.operand, BinaryOp) and expr.operand.op in ('and', 'or'):
|
|
694
|
+
return f'{op}({operand})'
|
|
695
|
+
|
|
696
|
+
return f'{op}{operand}'
|
|
697
|
+
|
|
698
|
+
def generate_ternary_op(self, expr: TernaryOp) -> str:
|
|
699
|
+
"""Generate ternary operation (Python if/else expression)."""
|
|
700
|
+
condition = self.generate_expression(expr.condition)
|
|
701
|
+
true_expr = self.generate_expression(expr.true_expr)
|
|
702
|
+
false_expr = self.generate_expression(expr.false_expr) if expr.false_expr else 'None'
|
|
703
|
+
|
|
704
|
+
return f'({true_expr} if {condition} else {false_expr})'
|
|
705
|
+
|
|
706
|
+
def generate_function_call(self, call: FunctionCall) -> str:
|
|
707
|
+
"""Generate function call."""
|
|
708
|
+
# Generate function name
|
|
709
|
+
if isinstance(call.func, str):
|
|
710
|
+
func = call.func
|
|
711
|
+
elif isinstance(call.func, MemberAccess):
|
|
712
|
+
func = self.generate_member_access(call.func)
|
|
713
|
+
elif isinstance(call.func, NaLiteral):
|
|
714
|
+
# Special case: na(x) is a function call, not NA() literal
|
|
715
|
+
func = 'na'
|
|
716
|
+
# If NA() with no args, just return 'na' (for default params)
|
|
717
|
+
if not call.args and not call.kwargs:
|
|
718
|
+
return 'na'
|
|
719
|
+
else:
|
|
720
|
+
func = self.generate_expression(call.func)
|
|
721
|
+
|
|
722
|
+
# Generate arguments (positional - use single quotes for strings)
|
|
723
|
+
args = [self.generate_expression(arg) for arg in call.args]
|
|
724
|
+
|
|
725
|
+
# Generate keyword arguments (use double quotes for string literals)
|
|
726
|
+
kwargs = []
|
|
727
|
+
for key, value in call.kwargs.items():
|
|
728
|
+
# Special case: options parameter should use tuple () instead of list []
|
|
729
|
+
if key == 'options' and isinstance(value, ArrayLiteral):
|
|
730
|
+
elements = [self._generate_kwarg_value(e) for e in value.elements]
|
|
731
|
+
kwargs.append(f'{key}=({", ".join(elements)})')
|
|
732
|
+
else:
|
|
733
|
+
kwargs.append(f'{key}={self._generate_kwarg_value(value)}')
|
|
734
|
+
|
|
735
|
+
all_args = args + kwargs
|
|
736
|
+
return f'{func}({", ".join(all_args)})'
|
|
737
|
+
|
|
738
|
+
def _generate_kwarg_value(self, value: Expression) -> str:
|
|
739
|
+
"""Generate kwarg value with double quotes for strings."""
|
|
740
|
+
if isinstance(value, Literal) and value.literal_type == 'string':
|
|
741
|
+
return f'"{self._escape_string(value.value, chr(34))}"'
|
|
742
|
+
elif isinstance(value, Literal) and value.literal_type in ('int', 'float', 'bool'):
|
|
743
|
+
# For non-string literals, use normal generation
|
|
744
|
+
return self.generate_literal(value)
|
|
745
|
+
else:
|
|
746
|
+
return self.generate_expression(value)
|
|
747
|
+
|
|
748
|
+
def generate_method_call(self, call: MethodCall) -> str:
|
|
749
|
+
"""Generate method call."""
|
|
750
|
+
from .pine_builtins import PYNECORE_METHOD_TRANSFORMS
|
|
751
|
+
from .ast_nodes import BinaryOp, UnaryOp, TernaryOp, IfExpression
|
|
752
|
+
|
|
753
|
+
obj = self.generate_expression(call.object)
|
|
754
|
+
args = [self.generate_expression(arg) for arg in call.args]
|
|
755
|
+
kwargs = [f'{key}={self.generate_expression(value)}' for key, value in call.kwargs.items()]
|
|
756
|
+
|
|
757
|
+
# Check if this method should be transformed to a module function call
|
|
758
|
+
# (e.g., m.add_row(...) → matrix.add_row(m, ...))
|
|
759
|
+
if call.method in PYNECORE_METHOD_TRANSFORMS:
|
|
760
|
+
module = PYNECORE_METHOD_TRANSFORMS[call.method]
|
|
761
|
+
# Transform to module.function(obj, args...)
|
|
762
|
+
all_args = [obj] + args + kwargs
|
|
763
|
+
return f'{module}.{call.method}({", ".join(all_args)})'
|
|
764
|
+
|
|
765
|
+
# FIX: Wrap complex expressions in parentheses to preserve precedence
|
|
766
|
+
# This ensures (expression).method() keeps correct semantics
|
|
767
|
+
if isinstance(call.object, (BinaryOp, UnaryOp, TernaryOp, IfExpression)):
|
|
768
|
+
obj = f'({obj})'
|
|
769
|
+
|
|
770
|
+
# Otherwise, keep as method call
|
|
771
|
+
all_args = args + kwargs
|
|
772
|
+
return f'{obj}.{call.method}({", ".join(all_args)})'
|
|
773
|
+
|
|
774
|
+
def generate_member_access(self, expr: MemberAccess) -> str:
|
|
775
|
+
"""Generate member access."""
|
|
776
|
+
from .pine_builtins import TA_VARIABLE_FUNCTIONS
|
|
777
|
+
|
|
778
|
+
if isinstance(expr.object, str):
|
|
779
|
+
obj = expr.object
|
|
780
|
+
elif isinstance(expr.object, Expression):
|
|
781
|
+
obj = self.generate_expression(expr.object)
|
|
782
|
+
else:
|
|
783
|
+
obj = str(expr.object)
|
|
784
|
+
|
|
785
|
+
result = f'{obj}.{expr.member}'
|
|
786
|
+
|
|
787
|
+
# Pine Script "variable-functions": used without () in Pine Script
|
|
788
|
+
# but implemented as functions in PyneCore (e.g., ta.vwap -> ta.vwap())
|
|
789
|
+
if result in TA_VARIABLE_FUNCTIONS:
|
|
790
|
+
defaults = TA_VARIABLE_FUNCTIONS.get(result, '')
|
|
791
|
+
result = f'{result}({defaults})'
|
|
792
|
+
|
|
793
|
+
return result
|
|
794
|
+
|
|
795
|
+
def generate_index_access(self, expr: IndexAccess) -> str:
|
|
796
|
+
"""Generate index access."""
|
|
797
|
+
obj = self.generate_expression(expr.object)
|
|
798
|
+
index = self.generate_expression(expr.index)
|
|
799
|
+
return f'{obj}[{index}]'
|
|
800
|
+
|
|
801
|
+
def generate_array_literal(self, expr: ArrayLiteral) -> str:
|
|
802
|
+
"""Generate array literal."""
|
|
803
|
+
elements = [self.generate_expression(e) for e in expr.elements]
|
|
804
|
+
return f'[{", ".join(elements)}]'
|
|
805
|
+
|
|
806
|
+
def _escape_string(self, value: str, quote: str) -> str:
|
|
807
|
+
"""Escape a string value for Python output."""
|
|
808
|
+
escaped = value.replace('\\', '\\\\')
|
|
809
|
+
escaped = escaped.replace('\n', '\\n')
|
|
810
|
+
escaped = escaped.replace('\t', '\\t')
|
|
811
|
+
escaped = escaped.replace('\r', '\\r')
|
|
812
|
+
escaped = escaped.replace(quote, f'\\{quote}')
|
|
813
|
+
return escaped
|
|
814
|
+
|
|
815
|
+
def generate_literal(self, expr: Literal) -> str:
|
|
816
|
+
"""Generate literal value."""
|
|
817
|
+
if expr.literal_type == 'string':
|
|
818
|
+
if expr.is_double_quoted:
|
|
819
|
+
return f'"{self._escape_string(expr.value, chr(34))}"'
|
|
820
|
+
else:
|
|
821
|
+
return f"'{self._escape_string(expr.value, chr(39))}'"
|
|
822
|
+
elif expr.literal_type == 'bool':
|
|
823
|
+
return 'True' if expr.value else 'False'
|
|
824
|
+
elif expr.literal_type == 'color':
|
|
825
|
+
if expr.is_double_quoted:
|
|
826
|
+
return f'"{self._escape_string(expr.value, chr(34))}"'
|
|
827
|
+
else:
|
|
828
|
+
return f"'{self._escape_string(expr.value, chr(39))}'"
|
|
829
|
+
else:
|
|
830
|
+
return str(expr.value)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def generate_code(script: PyneTransformedScript) -> str:
|
|
834
|
+
"""Convenience function to generate Python code from transformed script."""
|
|
835
|
+
codegen = CodeGenerator()
|
|
836
|
+
return codegen.generate(script)
|