pine2pyne 0.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. pine2pyne/__init__.py +84 -0
  2. pine2pyne/__main__.py +12 -0
  3. pine2pyne/ast_nodes.py +405 -0
  4. pine2pyne/ast_walk.py +93 -0
  5. pine2pyne/block_value_lowering.py +694 -0
  6. pine2pyne/bool_builtins.py +42 -0
  7. pine2pyne/bool_value_typing.py +580 -0
  8. pine2pyne/cli.py +247 -0
  9. pine2pyne/codegen.py +886 -0
  10. pine2pyne/compat/__init__.py +19 -0
  11. pine2pyne/compat/__main__.py +6 -0
  12. pine2pyne/compat/cli.py +114 -0
  13. pine2pyne/compat/comparison.py +105 -0
  14. pine2pyne/compat/corpus.py +73 -0
  15. pine2pyne/compat/report.py +95 -0
  16. pine2pyne/compat/runner.py +63 -0
  17. pine2pyne/condition_typing.py +260 -0
  18. pine2pyne/conditional_history_calls.py +96 -0
  19. pine2pyne/core/__init__.py +26 -0
  20. pine2pyne/core/contracts.py +125 -0
  21. pine2pyne/di.py +173 -0
  22. pine2pyne/emit/__init__.py +5 -0
  23. pine2pyne/emit/legacy.py +14 -0
  24. pine2pyne/emit/pynecomp/__init__.py +20 -0
  25. pine2pyne/emit/pynecomp/banner.py +30 -0
  26. pine2pyne/emit/pynecomp/blank_lines.py +69 -0
  27. pine2pyne/emit/pynecomp/emitter.py +61 -0
  28. pine2pyne/emit/pynecomp/imports.py +65 -0
  29. pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
  30. pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
  31. pine2pyne/emit/pynecomp/passes/base.py +69 -0
  32. pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
  33. pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
  34. pine2pyne/emit/pynecomp/passes/imports.py +112 -0
  35. pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
  36. pine2pyne/emit/pynecomp/passes/library.py +205 -0
  37. pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
  38. pine2pyne/emit/pynecomp/printer.py +288 -0
  39. pine2pyne/emit/pynecomp/unparser.py +186 -0
  40. pine2pyne/emitted_names.py +152 -0
  41. pine2pyne/errors.py +69 -0
  42. pine2pyne/for_loop_end_bound.py +172 -0
  43. pine2pyne/grouped_history_lowering.py +310 -0
  44. pine2pyne/import_resolver.py +469 -0
  45. pine2pyne/input_hoisting.py +195 -0
  46. pine2pyne/legacy_function_names.py +74 -0
  47. pine2pyne/legacy_sessions.py +68 -0
  48. pine2pyne/lexer.py +630 -0
  49. pine2pyne/library_collision.py +434 -0
  50. pine2pyne/namespace_members.py +247 -0
  51. pine2pyne/node_ids.py +235 -0
  52. pine2pyne/once_lowering.py +75 -0
  53. pine2pyne/overload_resolution.py +475 -0
  54. pine2pyne/parse_repair.py +398 -0
  55. pine2pyne/parser.py +1873 -0
  56. pine2pyne/pine_builtins.py +394 -0
  57. pine2pyne/pine_semantics.py +604 -0
  58. pine2pyne/pipeline/__init__.py +18 -0
  59. pine2pyne/pipeline/service.py +70 -0
  60. pine2pyne/pipeline/stages.py +73 -0
  61. pine2pyne/scope_renamer.py +965 -0
  62. pine2pyne/strategy_when_lowering.py +101 -0
  63. pine2pyne/strict_boolean.py +101 -0
  64. pine2pyne/symbol_table.py +166 -0
  65. pine2pyne/tokens.py +143 -0
  66. pine2pyne/transformer.py +2510 -0
  67. pine2pyne/type_inference.py +422 -0
  68. pine2pyne/udt_field_defaults.py +149 -0
  69. pine2pyne/value_blocks.py +357 -0
  70. pine2pyne-0.2.4.dist-info/METADATA +99 -0
  71. pine2pyne-0.2.4.dist-info/RECORD +75 -0
  72. pine2pyne-0.2.4.dist-info/WHEEL +5 -0
  73. pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
  74. pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
  75. pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,101 @@
1
+ """`when=` of a strategy call, lowered to an `if` (card #117).
2
+
3
+ strategy.entry("l", strategy.long, when=c) if c
4
+ -> strategy.entry("l", strategy.long)
5
+
6
+ Pine v1-v5 gave the order functions of `strategy.*` a `when` argument: the call acts only on the bars where it is
7
+ true. Pine v6 removed it, and the runtime's `strategy.entry` / `strategy.close` / ... take none, so a `when=` that
8
+ reached the generated code raised `TypeError: entry() got an unexpected keyword argument 'when'` at run. The
9
+ lowering runs at the end of parsing, so no later stage sees one.
10
+
11
+ Only the keyword form, only a call that stands as a statement (the functions return nothing), and only in a script
12
+ declaring //@version 1-5. A v6 script is left as written: TradingView refuses a `when` there, so no spelling is
13
+ invented for it. A positional `when` (the 9th slot of `strategy.entry`, the 2nd of v4 `strategy.close`) is not
14
+ lowered here (card #117 follow-up).
15
+ """
16
+ import dataclasses
17
+ import itertools
18
+
19
+ from .ast_nodes import (Assignment, ExpressionStatement, FuncDecl, FunctionCall, Identifier, IfStatement, IndexAccess,
20
+ MethodCall, Script)
21
+ from .ast_walk import child_field_names, contains, is_node
22
+
23
+ STRATEGY_FUNCTIONS_WITH_WHEN = frozenset({
24
+ 'strategy.entry', 'strategy.order', 'strategy.exit', 'strategy.close', 'strategy.close_all',
25
+ 'strategy.cancel', 'strategy.cancel_all',
26
+ })
27
+
28
+
29
+ def lower_strategy_when_arguments(script: Script, pine_version: int) -> Script:
30
+ """A copy of `script` with each `strategy.*(..., when=c)` statement turned into `if c` around the call
31
+ without `when`. A script without one, or of another version, is returned unchanged."""
32
+ if not 1 <= pine_version <= 5 or not contains(script, _is_call_with_when):
33
+ return script
34
+ return _WhenLowering().lowered(script)
35
+
36
+
37
+ def _is_call_with_when(node) -> bool:
38
+ # a callee that is not a name (`na(x)`: a NaLiteral node) is unhashable and is never a strategy function
39
+ return (isinstance(node, FunctionCall) and isinstance(node.func, str) and node.func in STRATEGY_FUNCTIONS_WITH_WHEN
40
+ and 'when' in node.kwargs)
41
+
42
+
43
+ def _is_stateful_or_history(node) -> bool:
44
+ return isinstance(node, (FunctionCall, MethodCall, IndexAccess))
45
+
46
+
47
+ class _Statements(list):
48
+ """The statements one statement is replaced by (a list item that expands to several)."""
49
+
50
+
51
+ class _WhenLowering:
52
+ """Pine evaluates every argument of a `strategy.*(..., when=c)` call on every bar, whatever `c` is. An argument
53
+ holding a call (`ta.lowest(low, 10)`, a user function) or a history index (`close[1]`) has state per bar, and
54
+ inside an `if` it would update only on the bars where `c` holds. So such an argument is assigned to a temporary
55
+ before the `if`, and the temporary is what the call inside the `if` receives."""
56
+
57
+ def __init__(self):
58
+ self.temporary_numbers = itertools.count(1)
59
+
60
+ def lowered(self, node):
61
+ children = {name: self._map(getattr(node, name)) for name in child_field_names(type(node))}
62
+ rebuilt = dataclasses.replace(node, **children)
63
+ if isinstance(rebuilt, ExpressionStatement) and _is_call_with_when(rebuilt.expr):
64
+ return _Statements(self._if_around(rebuilt))
65
+ if isinstance(rebuilt, FuncDecl) and _is_call_with_when(rebuilt.body):
66
+ # `f(c) => strategy.close("l", when=c)`: the one-line body is the call, made a statement
67
+ call = rebuilt.body
68
+ statement = ExpressionStatement(expr=call, line=call.line, column=call.column)
69
+ return dataclasses.replace(rebuilt, body=self._if_around(statement))
70
+ return rebuilt
71
+
72
+ def _map(self, value):
73
+ if isinstance(value, list):
74
+ items = []
75
+ for item in value:
76
+ mapped = self._map(item)
77
+ items.extend(mapped) if isinstance(mapped, _Statements) else items.append(mapped)
78
+ return items
79
+ if isinstance(value, tuple):
80
+ return tuple(self._map(item) for item in value)
81
+ if isinstance(value, dict):
82
+ return {key: self._map(item) for key, item in value.items()}
83
+ return self.lowered(value) if is_node(value) else value
84
+
85
+ def _if_around(self, statement: ExpressionStatement) -> list:
86
+ call = statement.expr
87
+ temporaries = []
88
+ args = [self._every_bar(argument, 'argument', statement, temporaries) for argument in call.args]
89
+ kwargs = {key: self._every_bar(value, key, statement, temporaries)
90
+ for key, value in call.kwargs.items() if key != 'when'}
91
+ without_when = dataclasses.replace(call, args=args, kwargs=kwargs)
92
+ guarded = IfStatement(condition=call.kwargs['when'], body=[dataclasses.replace(statement, expr=without_when)],
93
+ line=statement.line, column=statement.column)
94
+ return [*temporaries, guarded]
95
+
96
+ def _every_bar(self, argument, label, statement, temporaries):
97
+ if not contains(argument, _is_stateful_or_history):
98
+ return argument
99
+ name = f'_when_{label}_{next(self.temporary_numbers)}'
100
+ temporaries.append(Assignment(target=name, value=argument, line=statement.line, column=statement.column))
101
+ return Identifier(name=name, line=statement.line, column=statement.column)
@@ -0,0 +1,101 @@
1
+ """
2
+ Strict `and` / `or`: Pine v5 and earlier evaluate both operands (card #65).
3
+
4
+ The Pine v6 migration guide: "Pine v5 evaluates all bool expressions except for the `?:`
5
+ ternary operator strictly, meaning the second part of a conditional expression is always
6
+ evaluated, regardless of the value of the first argument"; v6 evaluates them lazily. Python's
7
+ `and` / `or` are lazy, so a stateful right operand (a `ta.*` call, a function with `var` or
8
+ history state) of a v5 script advanced its history only on the bars where it ran. Only v5 is
9
+ documented; v1-v4 are taken as strict like v5, and a script without a version annotation as
10
+ v6, the transpiler's target (as bool_is_never_na does).
11
+
12
+ The code generator emits `all((a, b, c))` / `any((a, b, c))` for a chain of one operator whose
13
+ operands after the first may have an effect: a tuple evaluates every element, left to right,
14
+ before `all` / `any` sees any. A chain whose later operands are pure (literals, names, history
15
+ reads, arithmetic, comparisons, `not`, `?:`, and calls of the builtins that keep no state:
16
+ `na`, `nz`, `math.*`, `str.*`, the strategy trade getters, collection getters) keeps the
17
+ Python `and` / `or`: skipping them changes nothing, and a getter run on an empty collection
18
+ (`strategy.opentrades.entry_price(0)` after a size check) must not start to run. A call of a
19
+ function the script declares itself is stateful: it may hold `var` state or call `ta.*`.
20
+ Every other call (`ta.*`, `request.*`, `fixnan`, a v4 `sma(...)`) is treated as stateful. This
21
+ module decides which.
22
+ """
23
+ from typing import FrozenSet, List, Optional
24
+
25
+ from .ast_nodes import (BinaryOp, Expression, FunctionCall, Identifier, IndexAccess, Literal, MemberAccess,
26
+ MethodCall, NaLiteral, Script, TernaryOp, UnaryOp)
27
+ from .ast_walk import walk
28
+ from .pine_builtins import TA_VARIABLE_FUNCTIONS
29
+
30
+ BOOLEAN_OPERATORS = frozenset({'and', 'or'})
31
+ # Nodes that cannot advance a history or change a variable. A call, a bound name (`:=`) or any
32
+ # node not listed is taken to have an effect
33
+ PURE_NODE_TYPES = (Literal, NaLiteral, Identifier, MemberAccess, IndexAccess, BinaryOp, UnaryOp, TernaryOp)
34
+ # Builtin calls that read their arguments and keep no state, by exact name and by namespace
35
+ PURE_FUNCTIONS = frozenset({'na', 'nz', 'int', 'float', 'bool', 'array.size', 'array.get', 'array.includes',
36
+ 'array.first', 'array.last', 'array.indexof', 'map.size', 'map.get', 'map.contains'})
37
+ PURE_NAMESPACES = ('math.', 'str.', 'string.', 'strategy.opentrades.', 'strategy.closedtrades.')
38
+ # `math.random` keeps its generator, `math.sum` its rolling window: both advance only on the bars that run them
39
+ STATEFUL_IN_PURE_NAMESPACES = frozenset({'math.random', 'math.sum'})
40
+ # The same getters called as methods: `values.size()`, `lookup.get(key)`
41
+ PURE_METHODS = frozenset({'size', 'get', 'includes', 'contains', 'first', 'last', 'indexof'})
42
+
43
+
44
+ def evaluates_and_or_strictly(script: Script) -> bool:
45
+ """True for Pine v5 and earlier; a script without a version annotation is v6."""
46
+ version = script.version.version if script.version else ''
47
+ return version.isdigit() and int(version) < 6
48
+
49
+
50
+ def _dotted_name(node) -> str:
51
+ if isinstance(node, str):
52
+ return node
53
+ if isinstance(node, Identifier):
54
+ return node.name
55
+ if isinstance(node, MemberAccess):
56
+ return f'{_dotted_name(node.object)}.{node.member}'
57
+ return ''
58
+
59
+
60
+ def _is_pure_call(node, user_function_names: FrozenSet[str]) -> bool:
61
+ if isinstance(node, MethodCall):
62
+ return node.method in PURE_METHODS and node.method not in user_function_names
63
+ name = 'na' if isinstance(node.func, NaLiteral) else _dotted_name(node.func)
64
+ if name in user_function_names:
65
+ return False
66
+ if name in PURE_FUNCTIONS:
67
+ return True
68
+ return name.startswith(PURE_NAMESPACES) and name not in STATEFUL_IN_PURE_NAMESPACES
69
+
70
+
71
+ def _is_pure(node, user_function_names: FrozenSet[str]) -> bool:
72
+ if isinstance(node, (FunctionCall, MethodCall)):
73
+ return _is_pure_call(node, user_function_names)
74
+ if not isinstance(node, PURE_NODE_TYPES):
75
+ return False
76
+ # `ta.tr`, `ta.vwap`, ... are written without () but are calls with state
77
+ return _dotted_name(node) not in TA_VARIABLE_FUNCTIONS
78
+
79
+
80
+ def may_have_effect(expression: Expression, user_function_names: FrozenSet[str]) -> bool:
81
+ """Whether evaluating `expression` can advance a history or bind a name."""
82
+ return any(not _is_pure(node, user_function_names) for node in walk(expression))
83
+
84
+
85
+ def _flatten(expression: Expression, operator: str, operands: List[Expression]) -> None:
86
+ """The operands of a chain of one operator, left to right; a nested group of the same
87
+ operator joins the chain, since strict evaluation is associative."""
88
+ if isinstance(expression, BinaryOp) and expression.op == operator:
89
+ _flatten(expression.left, operator, operands)
90
+ _flatten(expression.right, operator, operands)
91
+ else:
92
+ operands.append(expression)
93
+
94
+
95
+ def strict_operands(expression: BinaryOp, user_function_names: FrozenSet[str]) -> Optional[List[Expression]]:
96
+ """The operands to evaluate together, or None when the plain operator is enough."""
97
+ if expression.op not in BOOLEAN_OPERATORS:
98
+ return None
99
+ operands: List[Expression] = []
100
+ _flatten(expression, expression.op, operands)
101
+ return operands if any(may_have_effect(operand, user_function_names) for operand in operands[1:]) else None
@@ -0,0 +1,166 @@
1
+ """
2
+ Symbol table for tracking variable declarations and scopes.
3
+
4
+ Used by the transformer to determine variable types and manage scope.
5
+ """
6
+ from typing import Dict, List, Optional, Set
7
+ from enum import Enum
8
+
9
+
10
+ class VariableKind(Enum):
11
+ """Classification of variable kinds in Pine Script."""
12
+ VAR = "var" # Persistent variable (var keyword)
13
+ VARIP = "varip" # Persistent intrabar variable
14
+ INPUT = "input" # Input parameter
15
+ SERIES = "series" # Series variable (default for globals)
16
+ SIMPLE = "simple" # Simple (non-series) variable
17
+ FUNCTION = "function" # Function name
18
+ PARAMETER = "parameter" # Function parameter
19
+ TYPE = "type" # User-defined type (UDT) declaration
20
+
21
+
22
+ class Symbol:
23
+ """Represents a symbol (variable, function) in the symbol table."""
24
+
25
+ def __init__(
26
+ self,
27
+ name: str,
28
+ kind: VariableKind,
29
+ type_hint: Optional[str] = None,
30
+ is_global: bool = False,
31
+ line: int = 0,
32
+ column: int = 0,
33
+ ):
34
+ self.name = name
35
+ self.kind = kind
36
+ self.type_hint = type_hint
37
+ self.is_global = is_global
38
+ self.line = line
39
+ self.column = column
40
+ self.is_series = False # Will be set by type inference
41
+ self.is_indexed = False # True if variable is indexed with [n]
42
+ self.is_from_ta_function = False # True if assigned from ta.* function
43
+
44
+
45
+ class Scope:
46
+ """Represents a lexical scope."""
47
+
48
+ def __init__(self, parent: Optional['Scope'] = None):
49
+ self.parent = parent
50
+ self.symbols: Dict[str, Symbol] = {}
51
+
52
+ def define(self, symbol: Symbol) -> None:
53
+ """Define a symbol in this scope."""
54
+ self.symbols[symbol.name] = symbol
55
+
56
+ def lookup(self, name: str) -> Optional[Symbol]:
57
+ """Look up a symbol in this scope or parent scopes."""
58
+ if name in self.symbols:
59
+ return self.symbols[name]
60
+ if self.parent:
61
+ return self.parent.lookup(name)
62
+ return None
63
+
64
+ def lookup_local(self, name: str) -> Optional[Symbol]:
65
+ """Look up a symbol only in this scope (not parent)."""
66
+ return self.symbols.get(name)
67
+
68
+
69
+ class SymbolTable:
70
+ """Manages symbol definitions and scopes."""
71
+
72
+ def __init__(self):
73
+ self.global_scope = Scope()
74
+ self.current_scope = self.global_scope
75
+ self.builtin_symbols = self._init_builtins()
76
+ # UDT name -> field name -> declared (Python) type, from `type` declarations
77
+ self.udt_field_types: Dict[str, Dict[str, str]] = {}
78
+
79
+ def _init_builtins(self) -> Set[str]:
80
+ """Initialize built-in Pine Script variables."""
81
+ return {
82
+ # Price data
83
+ 'open', 'high', 'low', 'close', 'volume',
84
+ 'hl2', 'hlc3', 'ohlc4', 'hlcc4',
85
+ # Bar data
86
+ 'bar_index', 'time', 'timenow',
87
+ # Bar state
88
+ 'barstate.isfirst', 'barstate.islast', 'barstate.ishistory',
89
+ 'barstate.isrealtime', 'barstate.isnew', 'barstate.isconfirmed',
90
+ # Constants
91
+ 'na', 'true', 'false',
92
+ # Color constants
93
+ 'color.red', 'color.green', 'color.blue', 'color.white', 'color.black',
94
+ 'color.yellow', 'color.orange', 'color.purple', 'color.gray',
95
+ # Plot styles
96
+ 'plot.style_line', 'plot.style_stepline', 'plot.style_histogram',
97
+ 'plot.style_cross', 'plot.style_area', 'plot.style_columns',
98
+ 'plot.style_circles', 'plot.style_linebr', 'plot.style_steplinebr',
99
+ # Strategy constants
100
+ 'strategy.long', 'strategy.short',
101
+ 'strategy.commission.percent', 'strategy.commission.cash_per_contract',
102
+ 'strategy.commission.cash_per_order',
103
+ 'strategy.percent', 'strategy.fixed', 'strategy.cash',
104
+ 'strategy.percent_of_equity',
105
+ }
106
+
107
+ def enter_scope(self) -> None:
108
+ """Enter a new scope."""
109
+ self.current_scope = Scope(parent=self.current_scope)
110
+
111
+ def exit_scope(self) -> None:
112
+ """Exit the current scope."""
113
+ if self.current_scope.parent:
114
+ self.current_scope = self.current_scope.parent
115
+
116
+ def define(self, symbol: Symbol) -> None:
117
+ """Define a symbol in the current scope."""
118
+ self.current_scope.define(symbol)
119
+
120
+ def lookup(self, name: str) -> Optional[Symbol]:
121
+ """Look up a symbol in current or parent scopes, then among the builtins.
122
+
123
+ A user declaration that reuses a builtin name shadows it (Pine allows
124
+ `close = bar_index * 2`), so the nearest declared symbol wins and only an
125
+ undeclared builtin name resolves to a freshly synthesized SERIES symbol.
126
+ """
127
+ declared = self.current_scope.lookup(name)
128
+ if declared is not None:
129
+ return declared
130
+
131
+ if name in self.builtin_symbols:
132
+ return Symbol(name, VariableKind.SERIES, is_global=True)
133
+
134
+ return None
135
+
136
+ def lookup_global(self, name: str) -> Optional[Symbol]:
137
+ """Look up a symbol only in global scope."""
138
+ return self.global_scope.lookup_local(name)
139
+
140
+ def is_builtin(self, name: str) -> bool:
141
+ """Check if name is a built-in symbol."""
142
+ return name in self.builtin_symbols
143
+
144
+ def get_all_globals(self) -> List[Symbol]:
145
+ """Get all symbols defined in global scope."""
146
+ return list(self.global_scope.symbols.values())
147
+
148
+ def mark_as_indexed(self, name: str) -> None:
149
+ """Mark a variable as being indexed (e.g., close[1])."""
150
+ symbol = self.lookup(name)
151
+ if symbol:
152
+ symbol.is_indexed = True
153
+ symbol.is_series = True # Indexed variables are always Series
154
+
155
+ def mark_as_series(self, name: str) -> None:
156
+ """Mark a variable as a Series type."""
157
+ symbol = self.lookup(name)
158
+ if symbol:
159
+ symbol.is_series = True
160
+
161
+ def mark_from_ta_function(self, name: str) -> None:
162
+ """Mark a variable as assigned from a ta.* function."""
163
+ symbol = self.lookup(name)
164
+ if symbol:
165
+ symbol.is_from_ta_function = True
166
+ symbol.is_series = True # ta.* functions return Series
pine2pyne/tokens.py ADDED
@@ -0,0 +1,143 @@
1
+ """
2
+ Token type definitions for Pine Script v6 lexer.
3
+ """
4
+ from enum import Enum, auto
5
+ from dataclasses import dataclass
6
+ from typing import Any, Optional
7
+
8
+
9
+ class TokenType(Enum):
10
+ """All token types supported by the Pine Script v6 lexer."""
11
+
12
+ # Literals
13
+ INT_LITERAL = auto()
14
+ FLOAT_LITERAL = auto()
15
+ STRING_LITERAL = auto()
16
+ BOOL_LITERAL = auto()
17
+ COLOR_LITERAL = auto()
18
+ NA_LITERAL = auto()
19
+
20
+ # Identifiers and types
21
+ IDENTIFIER = auto()
22
+ TYPE_IDENTIFIER = auto()
23
+
24
+ # Keywords
25
+ VAR = auto()
26
+ VARIP = auto()
27
+ IF = auto()
28
+ ELSE = auto()
29
+ FOR = auto()
30
+ TO = auto()
31
+ BY = auto()
32
+ IN = auto()
33
+ WHILE = auto()
34
+ SWITCH = auto()
35
+ BREAK = auto()
36
+ CONTINUE = auto()
37
+ IMPORT = auto()
38
+ EXPORT = auto()
39
+ METHOD = auto()
40
+ TYPE = auto()
41
+ ENUM = auto()
42
+ NOT = auto()
43
+ AND = auto()
44
+ OR = auto()
45
+ FUNCTION = auto()
46
+ TRUE = auto()
47
+ FALSE = auto()
48
+
49
+ # Operators
50
+ ASSIGN = auto() # =
51
+ REASSIGN = auto() # :=
52
+ PLUS_ASSIGN = auto() # +=
53
+ MINUS_ASSIGN = auto() # -=
54
+ MULT_ASSIGN = auto() # *=
55
+ DIV_ASSIGN = auto() # /=
56
+ MOD_ASSIGN = auto() # %=
57
+ PLUS = auto() # +
58
+ MINUS = auto() # -
59
+ MULT = auto() # *
60
+ DIV = auto() # /
61
+ MOD = auto() # %
62
+ GT = auto() # >
63
+ LT = auto() # <
64
+ GTE = auto() # >=
65
+ LTE = auto() # <=
66
+ EQ = auto() # ==
67
+ NEQ = auto() # !=
68
+ ARROW = auto() # =>
69
+ TERNARY = auto() # ?
70
+ COLON = auto() # :
71
+ DOT = auto() # .
72
+
73
+ # Delimiters
74
+ LPAREN = auto() # (
75
+ RPAREN = auto() # )
76
+ LBRACKET = auto() # [
77
+ RBRACKET = auto() # ]
78
+ COMMA = auto() # ,
79
+ NEWLINE = auto() # \n
80
+ INDENT = auto() # Increase indentation
81
+ DEDENT = auto() # Decrease indentation
82
+ EOF = auto() # End of file
83
+
84
+ # Special
85
+ COMMENT = auto() # // comment
86
+ VERSION_ANNOTATION = auto() # //@version=6
87
+ COMPILER_ANNOTATION = auto() # //@description, //@function, etc.
88
+
89
+
90
+ # Keywords mapping
91
+ KEYWORDS = {
92
+ 'var': TokenType.VAR,
93
+ 'varip': TokenType.VARIP,
94
+ 'if': TokenType.IF,
95
+ 'else': TokenType.ELSE,
96
+ 'for': TokenType.FOR,
97
+ 'to': TokenType.TO,
98
+ 'by': TokenType.BY,
99
+ 'in': TokenType.IN,
100
+ 'while': TokenType.WHILE,
101
+ 'switch': TokenType.SWITCH,
102
+ 'break': TokenType.BREAK,
103
+ 'continue': TokenType.CONTINUE,
104
+ 'import': TokenType.IMPORT,
105
+ 'export': TokenType.EXPORT,
106
+ 'method': TokenType.METHOD,
107
+ 'type': TokenType.TYPE,
108
+ 'enum': TokenType.ENUM,
109
+ 'not': TokenType.NOT,
110
+ 'and': TokenType.AND,
111
+ 'or': TokenType.OR,
112
+ 'function': TokenType.FUNCTION,
113
+ 'true': TokenType.TRUE,
114
+ 'false': TokenType.FALSE,
115
+ 'na': TokenType.NA_LITERAL,
116
+ }
117
+
118
+ # Type identifiers (Pine Script built-in types)
119
+ TYPE_KEYWORDS = {
120
+ 'int', 'float', 'bool', 'string', 'color',
121
+ 'line', 'label', 'box', 'table', 'linefill', 'polyline',
122
+ 'array', 'matrix', 'map',
123
+ 'series', 'simple', 'input', 'const',
124
+ }
125
+
126
+
127
+ @dataclass
128
+ class Token:
129
+ """Represents a single token from the Pine Script source."""
130
+ type: TokenType
131
+ value: Any
132
+ line: int
133
+ column: int
134
+ lexeme: Optional[str] = None
135
+ is_double_quoted: bool = False # Track if string literal was double-quoted in source
136
+
137
+ def __repr__(self) -> str:
138
+ if self.lexeme:
139
+ return f"Token({self.type.name}, {self.value!r}, {self.line}:{self.column}, {self.lexeme!r})"
140
+ return f"Token({self.type.name}, {self.value!r}, {self.line}:{self.column})"
141
+
142
+ def __str__(self) -> str:
143
+ return self.__repr__()