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/__init__.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pine2pyne: Pine Script v6 to PyneCore Python transpiler.
|
|
3
|
+
|
|
4
|
+
A tool to convert TradingView Pine Script indicators and strategies
|
|
5
|
+
to PyneCore Python code for local backtesting and analysis.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = '0.1.0'
|
|
9
|
+
__author__ = 'Pine2Pyne Transpiler'
|
|
10
|
+
|
|
11
|
+
from .lexer import Lexer, tokenize
|
|
12
|
+
from .parser import Parser, parse
|
|
13
|
+
from .transformer import Transformer, PyneTransformedScript
|
|
14
|
+
from .codegen import CodeGenerator, generate_code
|
|
15
|
+
from .errors import (
|
|
16
|
+
Pine2PyneError,
|
|
17
|
+
LexerError,
|
|
18
|
+
ParserError,
|
|
19
|
+
TransformerError,
|
|
20
|
+
CodeGenError,
|
|
21
|
+
UnsupportedFeatureError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_comment_only(source: str) -> bool:
|
|
26
|
+
"""Check if source contains only comments and whitespace."""
|
|
27
|
+
for line in source.splitlines():
|
|
28
|
+
stripped = line.strip()
|
|
29
|
+
if stripped and not stripped.startswith('//'):
|
|
30
|
+
return False
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def transpile(source: str) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Transpile Pine Script source code to PyneCore Python.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
source: Pine Script v6 source code
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
PyneCore-compatible Python source code (empty string if source is comment-only)
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
Pine2PyneError: If any stage of transpilation fails
|
|
46
|
+
"""
|
|
47
|
+
# Skip empty or comment-only files
|
|
48
|
+
if not source or _is_comment_only(source):
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
# Tokenize
|
|
52
|
+
lexer = Lexer(source)
|
|
53
|
+
tokens = lexer.tokenize()
|
|
54
|
+
|
|
55
|
+
# Parse
|
|
56
|
+
parser = Parser(tokens)
|
|
57
|
+
ast = parser.parse()
|
|
58
|
+
|
|
59
|
+
# Transform
|
|
60
|
+
transformer = Transformer()
|
|
61
|
+
transformed = transformer.transform(ast)
|
|
62
|
+
|
|
63
|
+
# Generate code
|
|
64
|
+
codegen = CodeGenerator()
|
|
65
|
+
output = codegen.generate(transformed)
|
|
66
|
+
|
|
67
|
+
return output
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
'transpile',
|
|
72
|
+
'Lexer',
|
|
73
|
+
'Parser',
|
|
74
|
+
'Transformer',
|
|
75
|
+
'CodeGenerator',
|
|
76
|
+
'tokenize',
|
|
77
|
+
'parse',
|
|
78
|
+
'generate_code',
|
|
79
|
+
'Pine2PyneError',
|
|
80
|
+
'LexerError',
|
|
81
|
+
'ParserError',
|
|
82
|
+
'TransformerError',
|
|
83
|
+
'CodeGenError',
|
|
84
|
+
'UnsupportedFeatureError',
|
|
85
|
+
'PyneTransformedScript',
|
|
86
|
+
]
|
pine2pyne/__main__.py
ADDED
pine2pyne/ast_nodes.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST (Abstract Syntax Tree) node definitions for Pine Script v6.
|
|
3
|
+
|
|
4
|
+
All node types used by the parser to represent Pine Script syntax.
|
|
5
|
+
"""
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Base class for all AST nodes
|
|
11
|
+
@dataclass
|
|
12
|
+
class ASTNode:
|
|
13
|
+
"""Base class for all AST nodes."""
|
|
14
|
+
line: int = 0
|
|
15
|
+
column: int = 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ============================================================================
|
|
19
|
+
# Top-level nodes
|
|
20
|
+
# ============================================================================
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Script(ASTNode):
|
|
24
|
+
"""Root node representing the entire Pine Script."""
|
|
25
|
+
version: Optional['VersionAnnotation'] = None
|
|
26
|
+
script_decl: Optional[Union['IndicatorDecl', 'StrategyDecl', 'LibraryDecl']] = None
|
|
27
|
+
imports: List['ImportDecl'] = field(default_factory=list)
|
|
28
|
+
declarations: List[Union['VarDecl', 'VaripDecl', 'TypeDecl', 'EnumDecl', 'FuncDecl', 'InputDecl']] = field(default_factory=list)
|
|
29
|
+
body: List['Statement'] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class VersionAnnotation(ASTNode):
|
|
34
|
+
"""//@version=6 annotation."""
|
|
35
|
+
version: str = "" # "6" or "5"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class IndicatorDecl(ASTNode):
|
|
40
|
+
"""indicator("Title", ...) declaration."""
|
|
41
|
+
title: str = ""
|
|
42
|
+
kwargs: dict = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class StrategyDecl(ASTNode):
|
|
47
|
+
"""strategy("Title", ...) declaration."""
|
|
48
|
+
title: str = ""
|
|
49
|
+
kwargs: dict = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class LibraryDecl(ASTNode):
|
|
54
|
+
"""library("Title", ...) declaration."""
|
|
55
|
+
title: str = ""
|
|
56
|
+
kwargs: dict = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ImportDecl(ASTNode):
|
|
61
|
+
"""import user/library/version as alias"""
|
|
62
|
+
user: str = ""
|
|
63
|
+
library: str = ""
|
|
64
|
+
version: Optional[str] = None
|
|
65
|
+
alias: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ============================================================================
|
|
69
|
+
# Declarations
|
|
70
|
+
# ============================================================================
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class VarDecl(ASTNode):
|
|
74
|
+
"""var [type] name = value"""
|
|
75
|
+
name: str = ""
|
|
76
|
+
value: 'Expression' = None
|
|
77
|
+
type_hint: Optional[str] = None
|
|
78
|
+
is_var: bool = True # True for 'var', False for 'varip'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class VaripDecl(ASTNode):
|
|
83
|
+
"""varip [type] name = value"""
|
|
84
|
+
name: str = ""
|
|
85
|
+
value: 'Expression' = None
|
|
86
|
+
type_hint: Optional[str] = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class TypeDecl(ASTNode):
|
|
91
|
+
"""type MyType ... (User-defined type)"""
|
|
92
|
+
name: str = ""
|
|
93
|
+
fields: List[tuple] = field(default_factory=list) # [(field_name, type_hint, default_value), ...] or [(field_name, type_hint), ...]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class EnumDecl(ASTNode):
|
|
98
|
+
"""enum MyEnum ... (Enum definition)"""
|
|
99
|
+
name: str = ""
|
|
100
|
+
members: List = field(default_factory=list) # List[str] or List[tuple[str, Expression]]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class FuncDecl(ASTNode):
|
|
105
|
+
"""Function declaration: name(params) => body"""
|
|
106
|
+
name: str = ""
|
|
107
|
+
params: List['Parameter'] = field(default_factory=list)
|
|
108
|
+
body: Union['Expression', List['Statement']] = None
|
|
109
|
+
return_type: Optional[str] = None
|
|
110
|
+
is_method: bool = False
|
|
111
|
+
is_export: bool = False
|
|
112
|
+
indexed_params: set = field(default_factory=set) # Params that use [n] history access
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class Parameter(ASTNode):
|
|
117
|
+
"""Function parameter: [type] name [= default]"""
|
|
118
|
+
name: str = ""
|
|
119
|
+
type_hint: Optional[str] = None
|
|
120
|
+
default: Optional['Expression'] = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class InputDecl(ASTNode):
|
|
125
|
+
"""Input declaration: name = input.*(...)"""
|
|
126
|
+
name: str = ""
|
|
127
|
+
func: str = "" # e.g., "input.int"
|
|
128
|
+
args: List['Expression'] = field(default_factory=list)
|
|
129
|
+
kwargs: dict = field(default_factory=dict)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ============================================================================
|
|
133
|
+
# Expressions
|
|
134
|
+
# ============================================================================
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class Expression(ASTNode):
|
|
138
|
+
"""Base class for all expressions."""
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class BinaryOp(Expression):
|
|
144
|
+
"""Binary operation: left op right"""
|
|
145
|
+
left: Expression = None
|
|
146
|
+
op: str = ""
|
|
147
|
+
right: Expression = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class UnaryOp(Expression):
|
|
152
|
+
"""Unary operation: op operand"""
|
|
153
|
+
op: str = ""
|
|
154
|
+
operand: Expression = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class TernaryOp(Expression):
|
|
159
|
+
"""Ternary operation: condition ? true_expr : false_expr"""
|
|
160
|
+
condition: Expression = None
|
|
161
|
+
true_expr: Expression = None
|
|
162
|
+
false_expr: Expression = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class FunctionCall(Expression):
|
|
167
|
+
"""Function call: func(args, kwargs)"""
|
|
168
|
+
func: Union[str, 'MemberAccess'] = "" # Can be "ta.sma" or MemberAccess node
|
|
169
|
+
args: List[Expression] = field(default_factory=list)
|
|
170
|
+
kwargs: dict = field(default_factory=dict)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class MethodCall(Expression):
|
|
175
|
+
"""Method call: object.method(args)"""
|
|
176
|
+
object: Expression = None
|
|
177
|
+
method: str = ""
|
|
178
|
+
args: List[Expression] = field(default_factory=list)
|
|
179
|
+
kwargs: dict = field(default_factory=dict)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class MemberAccess(Expression):
|
|
184
|
+
"""Member access: object.member"""
|
|
185
|
+
object: Union[str, Expression] = "" # Can be identifier string or expression
|
|
186
|
+
member: str = ""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass
|
|
190
|
+
class IndexAccess(Expression):
|
|
191
|
+
"""Index access: object[index]"""
|
|
192
|
+
object: Expression = None
|
|
193
|
+
index: Expression = None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class ArrayLiteral(Expression):
|
|
198
|
+
"""Array literal: [elem1, elem2, ...]"""
|
|
199
|
+
elements: List[Expression] = field(default_factory=list)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class TupleDestructure(Expression):
|
|
204
|
+
"""Tuple destructuring: [a, b, c] = expr"""
|
|
205
|
+
names: List[str] = field(default_factory=list)
|
|
206
|
+
value: Expression = None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass
|
|
210
|
+
class Identifier(Expression):
|
|
211
|
+
"""Identifier (variable name)"""
|
|
212
|
+
name: str = ""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class Literal(Expression):
|
|
217
|
+
"""Literal value (int, float, string, bool, color)"""
|
|
218
|
+
value: Any = None
|
|
219
|
+
literal_type: str = "" # 'int', 'float', 'string', 'bool', 'color'
|
|
220
|
+
is_double_quoted: bool = False # Track if original source used double quotes
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass
|
|
224
|
+
class NaLiteral(Expression):
|
|
225
|
+
"""The 'na' literal value"""
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class IfExpression(Expression):
|
|
231
|
+
"""If expression (returns a value): if cond\n val1\nelse\n val2"""
|
|
232
|
+
condition: Expression = None
|
|
233
|
+
true_expr: Union[Expression, List['Statement']] = None
|
|
234
|
+
false_expr: Optional[Union[Expression, List['Statement']]] = None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass
|
|
238
|
+
class SwitchExpression(Expression):
|
|
239
|
+
"""Switch expression (returns a value)"""
|
|
240
|
+
expr: Optional[Expression] = None
|
|
241
|
+
cases: List[tuple[Expression, Union[Expression, List['Statement']]]] = field(default_factory=list)
|
|
242
|
+
default: Optional[Union[Expression, List['Statement']]] = None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ============================================================================
|
|
246
|
+
# Statements
|
|
247
|
+
# ============================================================================
|
|
248
|
+
|
|
249
|
+
@dataclass
|
|
250
|
+
class Statement(ASTNode):
|
|
251
|
+
"""Base class for all statements."""
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass
|
|
256
|
+
class Assignment(Statement):
|
|
257
|
+
"""Assignment statement: target = value"""
|
|
258
|
+
target: Union[str, 'TupleDestructure'] = ""
|
|
259
|
+
value: Expression = None
|
|
260
|
+
type_hint: Optional[str] = None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass
|
|
264
|
+
class Reassignment(Statement):
|
|
265
|
+
"""Reassignment statement: target := value"""
|
|
266
|
+
target: str = ""
|
|
267
|
+
value: Expression = None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class IfStatement(Statement):
|
|
272
|
+
"""If statement: if cond\n body\nelse if ...\nelse ..."""
|
|
273
|
+
condition: Expression = None
|
|
274
|
+
body: List[Statement] = field(default_factory=list)
|
|
275
|
+
elseifs: List[tuple[Expression, List[Statement]]] = field(default_factory=list)
|
|
276
|
+
else_body: Optional[List[Statement]] = None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@dataclass
|
|
280
|
+
class ForLoop(Statement):
|
|
281
|
+
"""For loop: for var = from to to_val [by step]"""
|
|
282
|
+
var: str = ""
|
|
283
|
+
from_val: Expression = None
|
|
284
|
+
to_val: Expression = None
|
|
285
|
+
step: Optional[Expression] = None
|
|
286
|
+
body: List[Statement] = field(default_factory=list)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass
|
|
290
|
+
class ForInLoop(Statement):
|
|
291
|
+
"""For-in loop: for [vars] in iterable"""
|
|
292
|
+
vars: List[str] = field(default_factory=list)
|
|
293
|
+
iterable: Expression = None
|
|
294
|
+
body: List[Statement] = field(default_factory=list)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@dataclass
|
|
298
|
+
class WhileLoop(Statement):
|
|
299
|
+
"""While loop: while condition\n body"""
|
|
300
|
+
condition: Expression = None
|
|
301
|
+
body: List[Statement] = field(default_factory=list)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@dataclass
|
|
305
|
+
class SwitchStatement(Statement):
|
|
306
|
+
"""Switch statement (imperative form)"""
|
|
307
|
+
expr: Optional[Expression] = None
|
|
308
|
+
cases: List[tuple[Expression, List[Statement]]] = field(default_factory=list)
|
|
309
|
+
default: Optional[List[Statement]] = None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass
|
|
313
|
+
class BreakStatement(Statement):
|
|
314
|
+
"""Break statement"""
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass
|
|
319
|
+
class ContinueStatement(Statement):
|
|
320
|
+
"""Continue statement"""
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@dataclass
|
|
325
|
+
class ExpressionStatement(Statement):
|
|
326
|
+
"""Expression as statement (e.g., function call on its own line)"""
|
|
327
|
+
expr: Expression = None
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@dataclass
|
|
331
|
+
class ReturnStatement(Statement):
|
|
332
|
+
"""Explicit return statement (for codegen)"""
|
|
333
|
+
expr: Optional[Expression] = None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@dataclass
|
|
337
|
+
class RawCode(Statement):
|
|
338
|
+
"""Raw Python code to be emitted as-is by codegen."""
|
|
339
|
+
code: str = ""
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ============================================================================
|
|
343
|
+
# Type annotations
|
|
344
|
+
# ============================================================================
|
|
345
|
+
|
|
346
|
+
# Forward references for type checking
|
|
347
|
+
Statement = Union[
|
|
348
|
+
Assignment, Reassignment, IfStatement, ForLoop, ForInLoop,
|
|
349
|
+
WhileLoop, SwitchStatement, BreakStatement, ContinueStatement,
|
|
350
|
+
ExpressionStatement, ReturnStatement, VarDecl, VaripDecl,
|
|
351
|
+
FuncDecl, InputDecl
|
|
352
|
+
]
|
pine2pyne/cli.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for pine2pyne transpiler.
|
|
3
|
+
"""
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
from . import transpile, __version__, _is_comment_only
|
|
9
|
+
from .errors import Pine2PyneError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace:
|
|
13
|
+
"""Parse command-line arguments."""
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog='pine2pyne',
|
|
16
|
+
description='Transpile Pine Script v6 to PyneCore Python',
|
|
17
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
18
|
+
epilog='''
|
|
19
|
+
Examples:
|
|
20
|
+
# Output to stdout
|
|
21
|
+
python -m pine2pyne strategy.pine
|
|
22
|
+
|
|
23
|
+
# Output to file
|
|
24
|
+
python -m pine2pyne strategy.pine -o strategy.py
|
|
25
|
+
|
|
26
|
+
# Validate syntax only
|
|
27
|
+
python -m pine2pyne strategy.pine --validate
|
|
28
|
+
|
|
29
|
+
# Batch convert multiple files
|
|
30
|
+
python -m pine2pyne strategy/*.pine -o output/
|
|
31
|
+
'''
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
'input',
|
|
36
|
+
nargs='+',
|
|
37
|
+
type=str,
|
|
38
|
+
help='Input Pine Script file(s) (.pine)'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
'-o', '--output',
|
|
43
|
+
type=str,
|
|
44
|
+
help='Output file or directory. If directory, outputs will have .py extension'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
'--validate',
|
|
49
|
+
action='store_true',
|
|
50
|
+
help='Validate syntax only (parse without generating code)'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
'-v', '--verbose',
|
|
55
|
+
action='store_true',
|
|
56
|
+
help='Enable verbose output'
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
'--version',
|
|
61
|
+
action='version',
|
|
62
|
+
version=f'%(prog)s {__version__}'
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return parser.parse_args(args)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def transpile_file(input_path: Path, output_path: Optional[Path], validate_only: bool, verbose: bool) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Transpile a single Pine Script file.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if successful, False otherwise
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
# Read input file
|
|
77
|
+
if verbose:
|
|
78
|
+
print(f'Reading {input_path}...')
|
|
79
|
+
|
|
80
|
+
source = input_path.read_text(encoding='utf-8')
|
|
81
|
+
|
|
82
|
+
if validate_only:
|
|
83
|
+
# Only parse to validate syntax
|
|
84
|
+
from .lexer import Lexer
|
|
85
|
+
from .parser import Parser
|
|
86
|
+
|
|
87
|
+
if verbose:
|
|
88
|
+
print('Tokenizing...')
|
|
89
|
+
lexer = Lexer(source)
|
|
90
|
+
tokens = lexer.tokenize()
|
|
91
|
+
|
|
92
|
+
if verbose:
|
|
93
|
+
print('Parsing...')
|
|
94
|
+
parser = Parser(tokens)
|
|
95
|
+
ast = parser.parse()
|
|
96
|
+
|
|
97
|
+
print(f'✓ {input_path} - Valid syntax')
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
# Full transpilation
|
|
101
|
+
if verbose:
|
|
102
|
+
print('Transpiling...')
|
|
103
|
+
|
|
104
|
+
output_code = transpile(source)
|
|
105
|
+
|
|
106
|
+
# Empty output is only legitimate for a comment-only / empty source.
|
|
107
|
+
# If the source carries real Pine code but the transpiler produced
|
|
108
|
+
# nothing, an unsupported construct was silently dropped — fail loudly
|
|
109
|
+
# instead of reporting a bogus success (exit 0 with no file written).
|
|
110
|
+
if not output_code:
|
|
111
|
+
if _is_comment_only(source):
|
|
112
|
+
if verbose:
|
|
113
|
+
print(f'Skipping {input_path} (comment-only file)')
|
|
114
|
+
return True
|
|
115
|
+
print(f'✗ {input_path}: transpiler produced no output for non-comment '
|
|
116
|
+
f'source (unsupported construct silently dropped)', file=sys.stderr)
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# Write output
|
|
120
|
+
if output_path:
|
|
121
|
+
if verbose:
|
|
122
|
+
print(f'Writing to {output_path}...')
|
|
123
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
output_path.write_text(output_code, encoding='utf-8')
|
|
125
|
+
print(f'✓ {input_path} → {output_path}')
|
|
126
|
+
else:
|
|
127
|
+
# Output to stdout
|
|
128
|
+
print(output_code)
|
|
129
|
+
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
except Pine2PyneError as e:
|
|
133
|
+
print(f'✗ {input_path}: {e}', file=sys.stderr)
|
|
134
|
+
return False
|
|
135
|
+
except FileNotFoundError:
|
|
136
|
+
print(f'✗ {input_path}: File not found', file=sys.stderr)
|
|
137
|
+
return False
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f'✗ {input_path}: Unexpected error: {e}', file=sys.stderr)
|
|
140
|
+
if verbose:
|
|
141
|
+
import traceback
|
|
142
|
+
traceback.print_exc()
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
147
|
+
"""Main entry point for CLI."""
|
|
148
|
+
args = parse_args(argv)
|
|
149
|
+
|
|
150
|
+
# Resolve input files
|
|
151
|
+
input_paths: List[Path] = []
|
|
152
|
+
for input_pattern in args.input:
|
|
153
|
+
path = Path(input_pattern)
|
|
154
|
+
if path.is_file():
|
|
155
|
+
input_paths.append(path)
|
|
156
|
+
elif '*' in input_pattern or '?' in input_pattern:
|
|
157
|
+
# Glob pattern
|
|
158
|
+
from glob import glob
|
|
159
|
+
for match in glob(input_pattern):
|
|
160
|
+
input_paths.append(Path(match))
|
|
161
|
+
else:
|
|
162
|
+
print(f'Error: {input_pattern} is not a file or valid pattern', file=sys.stderr)
|
|
163
|
+
return 1
|
|
164
|
+
|
|
165
|
+
if not input_paths:
|
|
166
|
+
print('Error: No input files found', file=sys.stderr)
|
|
167
|
+
return 1
|
|
168
|
+
|
|
169
|
+
# Determine output strategy
|
|
170
|
+
output_is_dir = False
|
|
171
|
+
output_base = None
|
|
172
|
+
|
|
173
|
+
if args.output:
|
|
174
|
+
output_path = Path(args.output)
|
|
175
|
+
if output_path.is_dir() or (not output_path.exists() and len(input_paths) > 1):
|
|
176
|
+
output_is_dir = True
|
|
177
|
+
output_base = output_path
|
|
178
|
+
elif len(input_paths) > 1:
|
|
179
|
+
print('Error: Multiple input files require output to be a directory', file=sys.stderr)
|
|
180
|
+
return 1
|
|
181
|
+
else:
|
|
182
|
+
output_base = output_path
|
|
183
|
+
|
|
184
|
+
# Process files
|
|
185
|
+
success_count = 0
|
|
186
|
+
fail_count = 0
|
|
187
|
+
|
|
188
|
+
for input_path in input_paths:
|
|
189
|
+
# Determine output path
|
|
190
|
+
if args.output is None:
|
|
191
|
+
output_path = None # stdout
|
|
192
|
+
elif output_is_dir:
|
|
193
|
+
output_name = input_path.stem + '.py'
|
|
194
|
+
output_path = output_base / output_name
|
|
195
|
+
else:
|
|
196
|
+
output_path = output_base
|
|
197
|
+
|
|
198
|
+
# Transpile
|
|
199
|
+
if transpile_file(input_path, output_path, args.validate, args.verbose):
|
|
200
|
+
success_count += 1
|
|
201
|
+
else:
|
|
202
|
+
fail_count += 1
|
|
203
|
+
|
|
204
|
+
# Print summary for multiple files
|
|
205
|
+
if len(input_paths) > 1:
|
|
206
|
+
print(f'\nSummary: {success_count} succeeded, {fail_count} failed')
|
|
207
|
+
|
|
208
|
+
return 0 if fail_count == 0 else 1
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == '__main__':
|
|
212
|
+
sys.exit(main())
|