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.
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)