pine2pyne 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pine2pyne/__init__.py +84 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +405 -0
- pine2pyne/ast_walk.py +93 -0
- pine2pyne/block_value_lowering.py +694 -0
- pine2pyne/bool_builtins.py +42 -0
- pine2pyne/bool_value_typing.py +580 -0
- pine2pyne/cli.py +247 -0
- pine2pyne/codegen.py +886 -0
- pine2pyne/compat/__init__.py +19 -0
- pine2pyne/compat/__main__.py +6 -0
- pine2pyne/compat/cli.py +114 -0
- pine2pyne/compat/comparison.py +105 -0
- pine2pyne/compat/corpus.py +73 -0
- pine2pyne/compat/report.py +95 -0
- pine2pyne/compat/runner.py +63 -0
- pine2pyne/condition_typing.py +260 -0
- pine2pyne/conditional_history_calls.py +96 -0
- pine2pyne/core/__init__.py +26 -0
- pine2pyne/core/contracts.py +125 -0
- pine2pyne/di.py +173 -0
- pine2pyne/emit/__init__.py +5 -0
- pine2pyne/emit/legacy.py +14 -0
- pine2pyne/emit/pynecomp/__init__.py +20 -0
- pine2pyne/emit/pynecomp/banner.py +30 -0
- pine2pyne/emit/pynecomp/blank_lines.py +69 -0
- pine2pyne/emit/pynecomp/emitter.py +61 -0
- pine2pyne/emit/pynecomp/imports.py +65 -0
- pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
- pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
- pine2pyne/emit/pynecomp/passes/base.py +69 -0
- pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
- pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
- pine2pyne/emit/pynecomp/passes/imports.py +112 -0
- pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
- pine2pyne/emit/pynecomp/passes/library.py +205 -0
- pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
- pine2pyne/emit/pynecomp/printer.py +288 -0
- pine2pyne/emit/pynecomp/unparser.py +186 -0
- pine2pyne/emitted_names.py +152 -0
- pine2pyne/errors.py +69 -0
- pine2pyne/for_loop_end_bound.py +172 -0
- pine2pyne/grouped_history_lowering.py +310 -0
- pine2pyne/import_resolver.py +469 -0
- pine2pyne/input_hoisting.py +195 -0
- pine2pyne/legacy_function_names.py +74 -0
- pine2pyne/legacy_sessions.py +68 -0
- pine2pyne/lexer.py +630 -0
- pine2pyne/library_collision.py +434 -0
- pine2pyne/namespace_members.py +247 -0
- pine2pyne/node_ids.py +235 -0
- pine2pyne/once_lowering.py +75 -0
- pine2pyne/overload_resolution.py +475 -0
- pine2pyne/parse_repair.py +398 -0
- pine2pyne/parser.py +1873 -0
- pine2pyne/pine_builtins.py +394 -0
- pine2pyne/pine_semantics.py +604 -0
- pine2pyne/pipeline/__init__.py +18 -0
- pine2pyne/pipeline/service.py +70 -0
- pine2pyne/pipeline/stages.py +73 -0
- pine2pyne/scope_renamer.py +965 -0
- pine2pyne/strategy_when_lowering.py +101 -0
- pine2pyne/strict_boolean.py +101 -0
- pine2pyne/symbol_table.py +166 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2510 -0
- pine2pyne/type_inference.py +422 -0
- pine2pyne/udt_field_defaults.py +149 -0
- pine2pyne/value_blocks.py +357 -0
- pine2pyne-0.2.4.dist-info/METADATA +99 -0
- pine2pyne-0.2.4.dist-info/RECORD +75 -0
- pine2pyne-0.2.4.dist-info/WHEEL +5 -0
- pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
- pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
- pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pine semantics: the checks TradingView's compiler makes that the grammar does not (card #28).
|
|
3
|
+
|
|
4
|
+
One read-only pass over the parsed Script, run by transpile() and by
|
|
5
|
+
`--validate` between the parser and the transformer. It keeps a Pine scope
|
|
6
|
+
model - the global scope, one scope per function (its parameters and body) and
|
|
7
|
+
one per if/for/while/switch body - and collects diagnostics at a Pine line and
|
|
8
|
+
1-based column: an identifier or call where one is at fault, else the
|
|
9
|
+
statement.
|
|
10
|
+
|
|
11
|
+
- A refusal is a program TradingView refuses whose transpile would give a
|
|
12
|
+
silent wrong value, drop a statement or fail at a Python line; enforce()
|
|
13
|
+
raises SemanticError for the first one and counts the rest.
|
|
14
|
+
- A warning is a program TradingView refuses that working files depend on
|
|
15
|
+
(operator decision 2026-09-24): a same-scope redeclaration, a read before the
|
|
16
|
+
declaration (from a function or block too) or in its own declaration.
|
|
17
|
+
enforce() reports each once per (line, column, name, code) as a
|
|
18
|
+
SemanticWarning, at its statement, when the script has no refusal; the
|
|
19
|
+
output is unchanged. Also a warning, though TradingView compiles it: a
|
|
20
|
+
`ta.*` call that runs on only some bars, whose history differs from the
|
|
21
|
+
every-bar call (conditional_history_calls.py, card #96). Never hoisted.
|
|
22
|
+
|
|
23
|
+
The pass never rewrites the tree, so a script it accepts transpiles exactly as
|
|
24
|
+
before. An assignment to a history reference (`x[1] := v`) is refused by the
|
|
25
|
+
parser instead: the tree keeps no trace of it.
|
|
26
|
+
|
|
27
|
+
This scope model is a second one beside scope_renamer's. scope_decisions()
|
|
28
|
+
exposes how it resolves each read, and a corpus test compares that with the
|
|
29
|
+
renamer's resolution, so a drift between the two fails.
|
|
30
|
+
"""
|
|
31
|
+
import dataclasses
|
|
32
|
+
import itertools
|
|
33
|
+
import typing
|
|
34
|
+
import warnings
|
|
35
|
+
from typing import Dict, List, NamedTuple, Optional, Tuple
|
|
36
|
+
|
|
37
|
+
from .ast_nodes import (
|
|
38
|
+
Assignment, EnumDecl, Expression, ExpressionStatement, ForInLoop, ForLoop, FuncDecl,
|
|
39
|
+
FunctionCall, Identifier, IfExpression, IfStatement, IndexAccess, InputDecl, Literal, MemberAccess,
|
|
40
|
+
MethodCall, Reassignment, ReturnStatement, Script, Statement, SwitchExpression,
|
|
41
|
+
SwitchStatement, TupleDestructure, TypeDecl, VarDecl, VaripDecl, WhileLoop,
|
|
42
|
+
)
|
|
43
|
+
from .ast_walk import walk
|
|
44
|
+
from .conditional_history_calls import conditional_history_calls
|
|
45
|
+
from .errors import SemanticError, SemanticWarning
|
|
46
|
+
from .library_collision import (
|
|
47
|
+
FieldReadRule, HeadBinding, declared_type, global_binding_types, script_type_evidence,
|
|
48
|
+
)
|
|
49
|
+
from .namespace_members import NAMESPACE_MEMBERS
|
|
50
|
+
from .pine_builtins import PYNECORE_LIB_MODULES, SERIES_VARIABLES, input_positional_param_names
|
|
51
|
+
from .symbol_table import SymbolTable
|
|
52
|
+
from .udt_field_defaults import constant_heads, default_message, invalid_defaults, positioned_part
|
|
53
|
+
|
|
54
|
+
REFUSE, WARN = 'refuse', 'warn'
|
|
55
|
+
# Before Pine v3 a declaration may read its own name (v2 `x = nz(x[1]) + 1`)
|
|
56
|
+
LAST_SELF_REFERENCE_VERSION = 2
|
|
57
|
+
DISCARD = '_'
|
|
58
|
+
# `_` discards a value from Pine v5 on; before v5 reading it is UNPROVEN, so it is not refused
|
|
59
|
+
DISCARD_FROM_VERSION = 5
|
|
60
|
+
# Pine v6 removed the history of a field of a user-defined type, `t.x[1]` (migration guide to v6, "History of
|
|
61
|
+
# UDT fields"); v5 accepts it
|
|
62
|
+
FIELD_HISTORY_REFUSED_FROM_VERSION = 6
|
|
63
|
+
# TradingView's Pine v6 compiler refuses a history read of a `for i = a to b` counter: MEASURED 2026-10-06
|
|
64
|
+
# (VN301! 5m): `Error at 5:31: Variable "i" doesn't have history values (CE10078)`, 5:31 being the counter in
|
|
65
|
+
# `nz(i[1], -1)` (card #99, item 1). Earlier versions and the variables of `for ... in` are not measured
|
|
66
|
+
LOOP_COUNTER_HISTORY_REFUSED_FROM_VERSION = 6
|
|
67
|
+
# Pine v6's namespaces: the heads of the dotted names of the TradingView v6
|
|
68
|
+
# reference (pinescript-docs-scraper pine_reference_types.json, 2026-03-12)
|
|
69
|
+
PINE_NAMESPACES = frozenset({
|
|
70
|
+
'adjustment', 'alert', 'array', 'backadjustment', 'barmerge', 'barstate', 'box', 'chart', 'color',
|
|
71
|
+
'currency', 'dayofweek', 'display', 'dividends', 'earnings', 'extend', 'font', 'footprint', 'format',
|
|
72
|
+
'hline', 'input', 'label', 'line', 'linefill', 'location', 'log', 'map', 'math', 'matrix', 'order',
|
|
73
|
+
'plot', 'polyline', 'position', 'request', 'runtime', 'scale', 'session', 'settlement_as_close',
|
|
74
|
+
'shape', 'size', 'splits', 'str', 'strategy', 'syminfo', 'ta', 'table', 'text', 'ticker',
|
|
75
|
+
'timeframe', 'volume_row', 'xloc', 'yloc',
|
|
76
|
+
})
|
|
77
|
+
# Pine v6's bare built-in variables (the same reference)
|
|
78
|
+
PINE_BUILTIN_VARIABLES = frozenset({
|
|
79
|
+
'ask', 'bar_index', 'bid', 'close', 'dayofmonth', 'dayofweek', 'false', 'high', 'hl2', 'hlc3',
|
|
80
|
+
'hlcc4', 'hour', 'last_bar_index', 'last_bar_time', 'low', 'minute', 'month', 'na', 'ohlc4', 'open',
|
|
81
|
+
'second', 'time', 'time_close', 'time_tradingday', 'timenow', 'true', 'volume', 'weekofyear', 'year',
|
|
82
|
+
})
|
|
83
|
+
# The bare built-in variables of Pine v4 and before that v5 moved into namespaces
|
|
84
|
+
LEGACY_BUILTIN_VARIABLES = frozenset({
|
|
85
|
+
'accdist', 'iii', 'interval', 'isdaily', 'isdwm', 'isintraday', 'ismonthly', 'isweekly', 'n', 'nii',
|
|
86
|
+
'nvi', 'obv', 'period', 'pvi', 'pvt', 'ticker', 'tickerid', 'tr', 'vwap', 'wad', 'wvad',
|
|
87
|
+
})
|
|
88
|
+
LAST_LEGACY_VERSION = 4
|
|
89
|
+
# Pine v3 and before: colours, styles, locations, shapes, sizes and days as bare constants
|
|
90
|
+
PRE_V4_CONSTANTS = frozenset({
|
|
91
|
+
'aqua', 'black', 'blue', 'fuchsia', 'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange',
|
|
92
|
+
'purple', 'red', 'silver', 'teal', 'white', 'yellow', 'line', 'stepline', 'histogram', 'cross', 'area',
|
|
93
|
+
'columns', 'circles', 'solid', 'dotted', 'dashed', 'abovebar', 'belowbar', 'top', 'bottom', 'absolute',
|
|
94
|
+
'xcross', 'circle', 'triangleup', 'triangledown', 'flag', 'arrowup', 'arrowdown', 'square', 'diamond',
|
|
95
|
+
'labelup', 'labeldown', 'auto', 'tiny', 'small', 'normal', 'large', 'huge', 'monday', 'tuesday',
|
|
96
|
+
'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
|
|
97
|
+
})
|
|
98
|
+
LAST_PRE_V4_VERSION = 3
|
|
99
|
+
# An enum cannot take a namespace's name (`str` and `input` included, docs 7144);
|
|
100
|
+
# a UDT can (docs 6876: "UDT names shadow the language's namespaces")
|
|
101
|
+
RESERVED_ENUM_NAMES = PINE_NAMESPACES
|
|
102
|
+
# Names a bare read may mean without a declaration in the script: never out of scope
|
|
103
|
+
BUILTIN_NAMES = (PINE_BUILTIN_VARIABLES | PINE_NAMESPACES | frozenset(SymbolTable().builtin_symbols)
|
|
104
|
+
| SERIES_VARIABLES | PYNECORE_LIB_MODULES | frozenset(NAMESPACE_MEMBERS))
|
|
105
|
+
PARAMETER, LOOP_VARIABLE, INPUT, VARIABLE = 'parameter', 'loop variable', 'input', 'variable'
|
|
106
|
+
LOOP_COUNTER = 'loop counter' # the variable of `for i = a to b`: a loop variable that has no history
|
|
107
|
+
STATEMENT_TYPES = typing.get_args(Statement)
|
|
108
|
+
SCOPE_KIND_TEXT = {'if': 'an if block', 'for': 'a for loop', 'while': 'a while loop',
|
|
109
|
+
'switch': 'a switch', 'function': 'a function'}
|
|
110
|
+
UNDECLARED_MESSAGE = ("'{}' is declared only inside a block or function that has ended here; "
|
|
111
|
+
"TradingView reports it as undeclared")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Diagnostic(NamedTuple):
|
|
115
|
+
severity: str
|
|
116
|
+
code: str
|
|
117
|
+
line: int
|
|
118
|
+
column: int
|
|
119
|
+
name: str
|
|
120
|
+
message: str
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ReadDecision(NamedTuple):
|
|
124
|
+
"""How the pass resolved one read: `binding` is (scope number, name), or
|
|
125
|
+
None when no declaration in the script is visible; `shadows` says whether
|
|
126
|
+
that binding hides one of the same name in an enclosing scope (a global
|
|
127
|
+
counts wherever it is declared, as scope_renamer binds every global first);
|
|
128
|
+
`later_global` marks an unresolved read of a global declared further down."""
|
|
129
|
+
binding: Optional[Tuple[int, str]]
|
|
130
|
+
shadows: bool
|
|
131
|
+
later_global: bool
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclasses.dataclass
|
|
135
|
+
class _Scope:
|
|
136
|
+
number: int
|
|
137
|
+
kind: str
|
|
138
|
+
names: Dict[str, str] = dataclasses.field(default_factory=dict) # name -> how it was declared
|
|
139
|
+
types: Dict[str, Optional[str]] = dataclasses.field(default_factory=dict) # name -> its declared Pine type
|
|
140
|
+
shadowing: set = dataclasses.field(default_factory=set) # names declared here that hide an outer one
|
|
141
|
+
declared_ahead: frozenset = frozenset() # names the scope's own statements declare, walked or not
|
|
142
|
+
ended: set = dataclasses.field(default_factory=set) # names bound in blocks nested here that have ended
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def check_script(script: Script) -> List[Diagnostic]:
|
|
146
|
+
"""Every diagnostic for `script`, in source order."""
|
|
147
|
+
return _SemanticCheck(script).run()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def enforce(script: Script) -> None:
|
|
151
|
+
"""Raise SemanticError for the first refusal, naming how many more there
|
|
152
|
+
are; a script with no refusal gets one SemanticWarning per (line, column,
|
|
153
|
+
name, code). A refused script is not warned about: the refusal is its
|
|
154
|
+
verdict, also under `-W error`."""
|
|
155
|
+
diagnostics = check_script(script)
|
|
156
|
+
refusals = [diagnostic for diagnostic in diagnostics if diagnostic.severity == REFUSE]
|
|
157
|
+
if refusals:
|
|
158
|
+
first = refusals[0]
|
|
159
|
+
raise SemanticError(first.code, first.message, first.line, first.column, len(refusals) - 1)
|
|
160
|
+
warned = {(diagnostic.line, diagnostic.column, diagnostic.name, diagnostic.code): diagnostic
|
|
161
|
+
for diagnostic in diagnostics if diagnostic.severity == WARN}
|
|
162
|
+
for diagnostic in warned.values():
|
|
163
|
+
warnings.warn(SemanticWarning(diagnostic.code, diagnostic.message, diagnostic.line, diagnostic.column),
|
|
164
|
+
stacklevel=3)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def scope_decisions(script: Script) -> Dict[Tuple[int, int, str], ReadDecision]:
|
|
168
|
+
"""(line, column, name) of every positioned bare-name read -> how the pass resolved it."""
|
|
169
|
+
check = _SemanticCheck(script)
|
|
170
|
+
check.run()
|
|
171
|
+
return check.decisions
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _declared_names(node) -> List[str]:
|
|
175
|
+
if isinstance(node, (VarDecl, VaripDecl, InputDecl)):
|
|
176
|
+
return [node.name]
|
|
177
|
+
if isinstance(node, Assignment):
|
|
178
|
+
return list(node.target.names) if isinstance(node.target, TupleDestructure) else [node.target]
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _pine_version(script: Script) -> int:
|
|
183
|
+
version = script.version.version if script.version else ''
|
|
184
|
+
return int(version) if version.isdigit() else 1 # no annotation: Pine v1
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class _SemanticCheck:
|
|
188
|
+
def __init__(self, script: Script):
|
|
189
|
+
self.script = script
|
|
190
|
+
self.version = _pine_version(script)
|
|
191
|
+
self.constant_heads = constant_heads(script)
|
|
192
|
+
self.builtin_names = BUILTIN_NAMES | (LEGACY_BUILTIN_VARIABLES if self.version <= LAST_LEGACY_VERSION
|
|
193
|
+
else frozenset()) \
|
|
194
|
+
| (PRE_V4_CONSTANTS if self.version <= LAST_PRE_V4_VERSION else frozenset())
|
|
195
|
+
self.top_items = sorted(list(script.declarations) + list(script.body), key=lambda item: item.line)
|
|
196
|
+
self.diagnostics: List[Diagnostic] = []
|
|
197
|
+
self.decisions: Dict[Tuple[int, int, str], ReadDecision] = {}
|
|
198
|
+
self.line, self.column = 0, 0
|
|
199
|
+
self.declaring: frozenset = frozenset() # the names the statement being walked declares
|
|
200
|
+
self.scope_numbers = itertools.count()
|
|
201
|
+
self.global_names = {name for item in self.top_items for name in _declared_names(item)}
|
|
202
|
+
self.field_read_rule = FieldReadRule(script)
|
|
203
|
+
self.global_types = global_binding_types(script)
|
|
204
|
+
self.type_evidence = script_type_evidence(script)
|
|
205
|
+
every_node = list(walk(self.top_items))
|
|
206
|
+
self.local_only = _names_declared_in(every_node) - self.global_names
|
|
207
|
+
self.function_names = {node.name for node in every_node if isinstance(node, FuncDecl)}
|
|
208
|
+
self.type_names = {node.name for node in every_node if isinstance(node, (TypeDecl, EnumDecl))}
|
|
209
|
+
declared_count: Dict[str, int] = {}
|
|
210
|
+
for item in self.top_items:
|
|
211
|
+
if isinstance(item, FuncDecl) and not item.is_method:
|
|
212
|
+
declared_count[item.name] = declared_count.get(item.name, 0) + 1
|
|
213
|
+
self.overloaded = {name for name, count in declared_count.items() if count > 1}
|
|
214
|
+
|
|
215
|
+
# ------------------------------------------------------------ reporting
|
|
216
|
+
|
|
217
|
+
def report(self, severity: str, code: str, name: str, message: str, node=None) -> None:
|
|
218
|
+
"""A diagnostic at `node`'s own position when it has one, else the statement's."""
|
|
219
|
+
line, column = (node.line, node.column) if getattr(node, 'line', 0) else (self.line, self.column)
|
|
220
|
+
self.diagnostics.append(Diagnostic(severity, code, line, column, name, message))
|
|
221
|
+
|
|
222
|
+
def at(self, node) -> None:
|
|
223
|
+
if getattr(node, 'line', 0):
|
|
224
|
+
self.line, self.column = node.line, node.column
|
|
225
|
+
|
|
226
|
+
def at_expression(self, node) -> None:
|
|
227
|
+
"""An else-if condition or a switch case comes after a block that moved the
|
|
228
|
+
position: take its own, from its first positioned part."""
|
|
229
|
+
self.at(next((part for part in walk(node) if getattr(part, 'line', 0)), None))
|
|
230
|
+
|
|
231
|
+
# ------------------------------------------------------------ scopes
|
|
232
|
+
|
|
233
|
+
def new_scope(self, kind: str, statements=()) -> _Scope:
|
|
234
|
+
declared = [name for statement in statements for name in _declared_names(statement)] \
|
|
235
|
+
if isinstance(statements, list) else []
|
|
236
|
+
return _Scope(next(self.scope_numbers), kind, declared_ahead=frozenset(declared))
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def declared_later(stack: List[_Scope], name: str) -> bool:
|
|
240
|
+
"""`name` is declared further down a scope that is still open (it is not bound yet)."""
|
|
241
|
+
return any(name in scope.declared_ahead for scope in stack)
|
|
242
|
+
|
|
243
|
+
def out_of_scope(self, stack: List[_Scope], name: str) -> bool:
|
|
244
|
+
"""An unbound `name` that only a scope which has ended declared so far: bound in
|
|
245
|
+
an ended if/for/while/switch block of the reader's own scopes (the emitted
|
|
246
|
+
Python would read that block's stale value; a name the statement itself
|
|
247
|
+
declares is a self-reference instead), or local to other scopes and not
|
|
248
|
+
declared later in an open one."""
|
|
249
|
+
return ((name not in self.declaring and any(name in scope.ended for scope in self.own_scopes(stack)))
|
|
250
|
+
or (name in self.local_only and not self.declared_later(stack, name)))
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def own_scopes(stack: List[_Scope]) -> List[_Scope]:
|
|
254
|
+
"""The open scopes a read sees as its own: from the innermost function
|
|
255
|
+
scope in (a function reading a global is its own case), else all."""
|
|
256
|
+
functions = [index for index, scope in enumerate(stack) if scope.kind == 'function']
|
|
257
|
+
return stack[functions[-1]:] if functions else stack
|
|
258
|
+
|
|
259
|
+
def declare(self, stack: List[_Scope], name: str, how: str, type_name: Optional[str] = None) -> None:
|
|
260
|
+
if name == DISCARD:
|
|
261
|
+
return
|
|
262
|
+
scope = stack[-1]
|
|
263
|
+
earlier = scope.names.get(name)
|
|
264
|
+
if earlier is None:
|
|
265
|
+
outer = any(name in enclosing.names for enclosing in stack[:-1])
|
|
266
|
+
if outer or (len(stack) > 1 and name in self.global_names):
|
|
267
|
+
scope.shadowing.add(name)
|
|
268
|
+
elif INPUT in (earlier, how): # an input is declared only at global scope (parser.py top-level loop)
|
|
269
|
+
self.report(REFUSE, 'duplicate-input', name, f"'{name}' is already declared in this scope "
|
|
270
|
+
f'(an input and another declaration cannot share a name)')
|
|
271
|
+
elif earlier not in (LOOP_VARIABLE, LOOP_COUNTER): # a parameter too: UNPROVEN on TradingView, so a warning
|
|
272
|
+
self.report(WARN, 'redeclared', name, f"'{name}' is already declared in this scope; "
|
|
273
|
+
f'TradingView refuses a redeclaration, the later one is used here')
|
|
274
|
+
scope.names.setdefault(name, how)
|
|
275
|
+
scope.types.setdefault(name, type_name) # the first declaration's, as scope_renamer binds it
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def depth_of(stack: List[_Scope], name: str) -> Optional[int]:
|
|
279
|
+
for depth in range(len(stack) - 1, -1, -1):
|
|
280
|
+
if name in stack[depth].names:
|
|
281
|
+
return depth
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
def read(self, stack: List[_Scope], identifier: Identifier) -> None:
|
|
285
|
+
name = identifier.name
|
|
286
|
+
depth = self.depth_of(stack, name)
|
|
287
|
+
if identifier.line:
|
|
288
|
+
binding = (stack[depth].number, name) if depth is not None else None
|
|
289
|
+
shadows = depth is not None and name in stack[depth].shadowing
|
|
290
|
+
later_global = depth is None and name in self.global_names
|
|
291
|
+
self.decisions[identifier.line, identifier.column, name] = ReadDecision(binding, shadows, later_global)
|
|
292
|
+
if name == DISCARD:
|
|
293
|
+
if self.version >= DISCARD_FROM_VERSION:
|
|
294
|
+
self.report(REFUSE, 'discard-read', name, "'_' discards a value and cannot be read", identifier)
|
|
295
|
+
return
|
|
296
|
+
if depth is not None or name in self.builtin_names or name in self.function_names or name in self.type_names:
|
|
297
|
+
return
|
|
298
|
+
if self.out_of_scope(stack, name):
|
|
299
|
+
self.report(REFUSE, 'out-of-scope', name, UNDECLARED_MESSAGE.format(name), identifier)
|
|
300
|
+
elif name in self.global_names or self.declared_later(stack, name):
|
|
301
|
+
self.read_before_declaration(name)
|
|
302
|
+
|
|
303
|
+
def read_before_declaration(self, name: str) -> None:
|
|
304
|
+
"""A read above the name's declaration in a scope that is still open, from
|
|
305
|
+
any inner scope (a function reading a later global is UNPROVEN on
|
|
306
|
+
TradingView), or in its own declaration."""
|
|
307
|
+
if name not in self.declaring:
|
|
308
|
+
self.report(WARN, 'used-before-declaration', name, f"'{name}' is read before its declaration")
|
|
309
|
+
elif self.version > LAST_SELF_REFERENCE_VERSION:
|
|
310
|
+
self.report(WARN, 'self-reference', name, f"'{name}' is read in its own declaration")
|
|
311
|
+
|
|
312
|
+
# ------------------------------------------------------------ statements
|
|
313
|
+
|
|
314
|
+
def run(self) -> List[Diagnostic]:
|
|
315
|
+
self.recursion()
|
|
316
|
+
stack = [self.new_scope('global')]
|
|
317
|
+
for item in self.top_items:
|
|
318
|
+
self.statement(item, stack)
|
|
319
|
+
self.conditional_history_calls()
|
|
320
|
+
return sorted(self.diagnostics, key=lambda diagnostic: (diagnostic.line, diagnostic.column))
|
|
321
|
+
|
|
322
|
+
def conditional_history_calls(self) -> None:
|
|
323
|
+
"""A `ta.*` call that runs on only some bars keeps a history that differs from the
|
|
324
|
+
every-bar call (card #96). A warning only: the call stays where the script has it."""
|
|
325
|
+
for call in conditional_history_calls(self.script):
|
|
326
|
+
self.report(WARN, 'conditional-history-call', call.func,
|
|
327
|
+
f"'{call.func}' is called only when its condition holds: its series history "
|
|
328
|
+
f'differs from an unconditional call (the output keeps the call where it is, as TradingView '
|
|
329
|
+
f'does; TradingView advises calling it on every bar, in the global scope)', call)
|
|
330
|
+
|
|
331
|
+
def block(self, statements, stack: List[_Scope], kind: str, names=(), how=LOOP_VARIABLE,
|
|
332
|
+
types: Optional[Dict[str, Optional[str]]] = None) -> None:
|
|
333
|
+
inner = stack + [self.new_scope(kind, statements if statements is not None else ())]
|
|
334
|
+
for name in names:
|
|
335
|
+
self.declare(inner, name, how, (types or {}).get(name))
|
|
336
|
+
if isinstance(statements, list):
|
|
337
|
+
for statement in statements:
|
|
338
|
+
self.statement(statement, inner)
|
|
339
|
+
elif statements is not None:
|
|
340
|
+
self.expression(statements, inner)
|
|
341
|
+
if kind != 'function': # a Python def keeps its locals; an if/for block leaks them
|
|
342
|
+
stack[-1].ended.update(inner[-1].names, inner[-1].ended)
|
|
343
|
+
|
|
344
|
+
def statement(self, node, stack: List[_Scope]) -> None:
|
|
345
|
+
self.at(node)
|
|
346
|
+
if isinstance(node, FuncDecl):
|
|
347
|
+
self.function(node, stack)
|
|
348
|
+
elif isinstance(node, (VarDecl, VaripDecl, Assignment)):
|
|
349
|
+
enclosing = self.declaring
|
|
350
|
+
self.declaring = enclosing | frozenset(_declared_names(node))
|
|
351
|
+
self.expression(node.value, stack)
|
|
352
|
+
self.declaring = enclosing
|
|
353
|
+
self.at(node) # a block in the value moved the position
|
|
354
|
+
for name in _declared_names(node):
|
|
355
|
+
self.declare(stack, name, VARIABLE, declared_type(node, self.type_evidence._replace(
|
|
356
|
+
variable_type=lambda variable: self.variable_type(variable, stack))))
|
|
357
|
+
elif isinstance(node, InputDecl):
|
|
358
|
+
self.expression([node.args, list(node.kwargs.values())], stack)
|
|
359
|
+
self.input_arguments(node, stack)
|
|
360
|
+
self.declare(stack, node.name, INPUT, declared_type(node))
|
|
361
|
+
elif isinstance(node, EnumDecl) and node.name in RESERVED_ENUM_NAMES:
|
|
362
|
+
self.report(REFUSE, 'named-like-namespace', node.name,
|
|
363
|
+
f"an enum cannot be named '{node.name}', the name of a built-in namespace")
|
|
364
|
+
elif isinstance(node, TypeDecl):
|
|
365
|
+
for field_name, default in invalid_defaults(node, self.constant_heads):
|
|
366
|
+
self.report(REFUSE, 'udt-field-default', field_name, default_message(node, field_name),
|
|
367
|
+
positioned_part(default))
|
|
368
|
+
elif isinstance(node, Reassignment):
|
|
369
|
+
self.expression(node.value, stack)
|
|
370
|
+
self.at(node) # a block in the value moved the position
|
|
371
|
+
self.reassignment(node.target, stack)
|
|
372
|
+
elif isinstance(node, (IfStatement, ForLoop, ForInLoop, WhileLoop)):
|
|
373
|
+
self.control_flow(node, stack)
|
|
374
|
+
elif isinstance(node, SwitchStatement):
|
|
375
|
+
self.switch(node, stack)
|
|
376
|
+
elif isinstance(node, (ExpressionStatement, ReturnStatement)):
|
|
377
|
+
self.expression(node.expr, stack)
|
|
378
|
+
elif isinstance(node, Expression):
|
|
379
|
+
self.expression(node, stack)
|
|
380
|
+
|
|
381
|
+
def input_arguments(self, node: InputDecl, stack: List[_Scope]) -> None:
|
|
382
|
+
"""An input's arguments must be const, so they cannot read another input (manual,
|
|
383
|
+
Inputs: "results from other input*() calls"); `active` takes an input bool, and
|
|
384
|
+
input.source's `defval` a series float (card #89)."""
|
|
385
|
+
parameters = ['defval', 'title'] + input_positional_param_names(node.func, node.args)
|
|
386
|
+
# a row that does not name `active` ends at `display`: a positional argument after it is `active`
|
|
387
|
+
positional = (parameters[:parameters.index('display') + 1]
|
|
388
|
+
if 'display' in parameters and 'active' not in parameters else parameters)
|
|
389
|
+
arguments = list(zip(positional, node.args)) + list(node.kwargs.items())
|
|
390
|
+
for parameter, argument in arguments:
|
|
391
|
+
if parameter == 'active' or (parameter == 'defval' and node.func == 'input.source'):
|
|
392
|
+
continue
|
|
393
|
+
for read in walk(argument):
|
|
394
|
+
if isinstance(read, Identifier) and self.depth_of(stack, read.name) == 0 \
|
|
395
|
+
and stack[0].names[read.name] == INPUT:
|
|
396
|
+
self.report(REFUSE, 'input-argument-reads-input', read.name,
|
|
397
|
+
f"the '{parameter}' argument of input '{node.name}' reads the input "
|
|
398
|
+
f"'{read.name}'; TradingView requires a const argument there", read)
|
|
399
|
+
|
|
400
|
+
def control_flow(self, node, stack: List[_Scope]) -> None:
|
|
401
|
+
if isinstance(node, IfStatement):
|
|
402
|
+
self.expression(node.condition, stack)
|
|
403
|
+
self.block(node.body, stack, 'if')
|
|
404
|
+
for condition, body in node.elseifs:
|
|
405
|
+
self.at_expression(condition)
|
|
406
|
+
self.expression(condition, stack)
|
|
407
|
+
self.block(body, stack, 'if')
|
|
408
|
+
self.block(node.else_body, stack, 'if')
|
|
409
|
+
elif isinstance(node, ForLoop):
|
|
410
|
+
self.expression([node.from_val, node.to_val, node.step], stack)
|
|
411
|
+
self.block(node.body, stack, 'for', [node.var], LOOP_COUNTER)
|
|
412
|
+
elif isinstance(node, ForInLoop):
|
|
413
|
+
self.expression(node.iterable, stack)
|
|
414
|
+
self.block(node.body, stack, 'for', node.vars)
|
|
415
|
+
else:
|
|
416
|
+
self.expression(node.condition, stack)
|
|
417
|
+
self.block(node.body, stack, 'while')
|
|
418
|
+
|
|
419
|
+
def function(self, node: FuncDecl, stack: List[_Scope]) -> None:
|
|
420
|
+
if len(stack) > 1:
|
|
421
|
+
self.report(REFUSE, 'nested-function', node.name, f"function '{node.name}' is declared inside "
|
|
422
|
+
f'{SCOPE_KIND_TEXT[stack[-1].kind]}; TradingView allows a function declaration only '
|
|
423
|
+
f'at global scope')
|
|
424
|
+
self.expression([parameter.default for parameter in node.params], stack)
|
|
425
|
+
self.block(node.body, stack, 'function', [parameter.name for parameter in node.params], PARAMETER,
|
|
426
|
+
{parameter.name: declared_type(parameter) for parameter in node.params})
|
|
427
|
+
|
|
428
|
+
def reassignment(self, target: str, stack: List[_Scope]) -> None:
|
|
429
|
+
if '.' in target: # a field: `p.v := x` is valid anywhere
|
|
430
|
+
return
|
|
431
|
+
depth = self.depth_of(stack, target)
|
|
432
|
+
functions = [index for index, scope in enumerate(stack) if scope.kind == 'function']
|
|
433
|
+
if depth is None:
|
|
434
|
+
if self.out_of_scope(stack, target):
|
|
435
|
+
self.report(REFUSE, 'out-of-scope', target, UNDECLARED_MESSAGE.format(target))
|
|
436
|
+
elif functions and target in self.global_names:
|
|
437
|
+
self.report(REFUSE, 'global-modified-in-function', target,
|
|
438
|
+
f"a function cannot modify the global variable '{target}'")
|
|
439
|
+
else:
|
|
440
|
+
self.report(REFUSE, 'reassigned-undeclared', target, f"'{target}' is not declared before "
|
|
441
|
+
f"this ':='; TradingView refuses a reassignment of an undeclared variable")
|
|
442
|
+
elif functions and depth < functions[-1]:
|
|
443
|
+
self.report(REFUSE, 'global-modified-in-function', target,
|
|
444
|
+
f"a function cannot modify the {'global' if depth == 0 else 'enclosing'} variable '{target}'")
|
|
445
|
+
elif stack[depth].names.get(target) == PARAMETER:
|
|
446
|
+
self.report(REFUSE, 'parameter-modified', target, f"a function cannot modify its parameter '{target}'")
|
|
447
|
+
|
|
448
|
+
def switch(self, node, stack: List[_Scope]) -> None:
|
|
449
|
+
self.expression(node.expr, stack)
|
|
450
|
+
for case, body in node.cases:
|
|
451
|
+
self.at_expression(case)
|
|
452
|
+
self.expression(case, stack)
|
|
453
|
+
self.block(body, stack, 'switch')
|
|
454
|
+
self.block(node.default, stack, 'switch')
|
|
455
|
+
|
|
456
|
+
# ------------------------------------------------------------ expressions
|
|
457
|
+
|
|
458
|
+
def expression(self, node, stack: List[_Scope]) -> None:
|
|
459
|
+
if node is None or isinstance(node, (str, int, float, bool)):
|
|
460
|
+
return
|
|
461
|
+
if isinstance(node, (list, tuple)):
|
|
462
|
+
for item in node:
|
|
463
|
+
self.expression(item, stack)
|
|
464
|
+
elif isinstance(node, dict):
|
|
465
|
+
self.expression(list(node.values()), stack)
|
|
466
|
+
elif isinstance(node, Identifier):
|
|
467
|
+
if '.' not in node.name: # `N.m` names a namespace, a type or a library unless N is in scope
|
|
468
|
+
self.read(stack, node)
|
|
469
|
+
elif isinstance(node, IndexAccess):
|
|
470
|
+
if isinstance(node.object, IndexAccess):
|
|
471
|
+
self.report(REFUSE, 'double-history', '', 'the [] history operator can be used only once on '
|
|
472
|
+
'the same value (close[1][2])', node)
|
|
473
|
+
elif self.version >= FIELD_HISTORY_REFUSED_FROM_VERSION and self.field_read_rule.is_field_read(
|
|
474
|
+
node.object, lambda name: self.binding_in_scope(name, stack)):
|
|
475
|
+
self.report(REFUSE, 'history-on-udt-field', '', self.field_history_message(node), node)
|
|
476
|
+
elif self.version >= LOOP_COUNTER_HISTORY_REFUSED_FROM_VERSION and self.is_loop_counter(node.object, stack):
|
|
477
|
+
self.report(REFUSE, 'loop-counter-history', node.object.name,
|
|
478
|
+
f'Variable "{node.object.name}" doesn\'t have history values', node.object)
|
|
479
|
+
self.expression([node.object, node.index], stack)
|
|
480
|
+
elif isinstance(node, FunctionCall):
|
|
481
|
+
if not isinstance(node.func, str):
|
|
482
|
+
self.expression(node.func, stack)
|
|
483
|
+
self.expression([node.args, list(node.kwargs.values())], stack)
|
|
484
|
+
elif isinstance(node, MethodCall):
|
|
485
|
+
self.expression([node.object, node.args, list(node.kwargs.values())], stack)
|
|
486
|
+
elif isinstance(node, MemberAccess):
|
|
487
|
+
if not isinstance(node.object, str):
|
|
488
|
+
self.expression(node.object, stack)
|
|
489
|
+
elif isinstance(node, IfExpression):
|
|
490
|
+
self.expression(node.condition, stack)
|
|
491
|
+
self.block(node.true_expr, stack, 'if')
|
|
492
|
+
self.block(node.false_expr, stack, 'if')
|
|
493
|
+
elif isinstance(node, SwitchExpression):
|
|
494
|
+
self.switch(node, stack)
|
|
495
|
+
elif isinstance(node, STATEMENT_TYPES):
|
|
496
|
+
self.statement(node, stack)
|
|
497
|
+
elif dataclasses.is_dataclass(node):
|
|
498
|
+
self.expression([getattr(node, field.name) for field in dataclasses.fields(node)
|
|
499
|
+
if field.name not in ('line', 'column')], stack)
|
|
500
|
+
|
|
501
|
+
def is_loop_counter(self, node, stack: List[_Scope]) -> bool:
|
|
502
|
+
"""`node` reads the counter of a `for i = a to b` loop that is open here: the innermost binding of
|
|
503
|
+
its name is the counter, so a name a nested block declares again is that declaration's, not the counter's.
|
|
504
|
+
A name the loop body declares again at the counter's own level keeps the counter's binding (a scope's
|
|
505
|
+
first declaration wins) and is refused: UNMEASURED on TradingView, which refuses the redeclaration itself."""
|
|
506
|
+
depth = self.depth_of(stack, node.name) if isinstance(node, Identifier) else None
|
|
507
|
+
return depth is not None and stack[depth].names[node.name] == LOOP_COUNTER
|
|
508
|
+
|
|
509
|
+
def binding_in_scope(self, name: str, stack: List[_Scope]) -> Optional[HeadBinding]:
|
|
510
|
+
"""The variable `name` names here, with its declared type: the binding in scope, or a
|
|
511
|
+
global (declared above or further down, typed as the scope renamer types it, by its
|
|
512
|
+
first global declaration); None when `name` is no variable here (card #88)."""
|
|
513
|
+
depth = self.depth_of(stack, name)
|
|
514
|
+
if depth is not None and depth > 0: # a global (depth 0) is typed below, in the renamer's order
|
|
515
|
+
return HeadBinding(stack[depth].types.get(name))
|
|
516
|
+
return HeadBinding(self.global_types.get(name)) if name in self.global_names else None
|
|
517
|
+
|
|
518
|
+
def variable_type(self, name: str, stack: List[_Scope]) -> Optional[str]:
|
|
519
|
+
"""The type of the variable `name` names here (`binding_in_scope`), None when unknown."""
|
|
520
|
+
binding = self.binding_in_scope(name, stack)
|
|
521
|
+
return binding.type_name if binding else None
|
|
522
|
+
|
|
523
|
+
@staticmethod
|
|
524
|
+
def field_history_message(site: IndexAccess) -> str:
|
|
525
|
+
"""The advice indexes the object, then walks the field path: `o.i.v[1]` -> `(o[1]).i.v`
|
|
526
|
+
(card #88; `(o.i[1]).v` would index a field again)."""
|
|
527
|
+
index = site.index.value if isinstance(site.index, Literal) else 'n'
|
|
528
|
+
spelled = site.object.name if isinstance(site.object, Identifier) else ''
|
|
529
|
+
if isinstance(site.object, MemberAccess) and isinstance(site.object.object, str):
|
|
530
|
+
spelled = f'{site.object.object}.{site.object.member}'
|
|
531
|
+
head, _, path = spelled.partition('.')
|
|
532
|
+
object_text, field_text = (head, path) if path else ('object', 'field')
|
|
533
|
+
return (f"the [] history operator cannot be used on a field of a user-defined type from Pine v6; "
|
|
534
|
+
f"write ({object_text}[{index}]).{field_text} for the field of the object {index} bar(s) back, "
|
|
535
|
+
f"or assign the field to a variable and index that variable")
|
|
536
|
+
|
|
537
|
+
# ------------------------------------------------------------ recursion
|
|
538
|
+
|
|
539
|
+
def recursion(self) -> None:
|
|
540
|
+
"""A call that closes a cycle in the call graph of the script's functions,
|
|
541
|
+
refused at the call. An overloaded name resolves by signature, so it is
|
|
542
|
+
left out of the graph (expds_097 calls one overload from another); an
|
|
543
|
+
overload that passes its own parameters, unchanged, to its own name
|
|
544
|
+
resolves to itself and is refused."""
|
|
545
|
+
declared = [item for item in self.top_items if isinstance(item, FuncDecl) and not item.is_method]
|
|
546
|
+
# a call by a method's name may select the method (`f(P.new(x))` beside `method f(P this)`)
|
|
547
|
+
method_names = {item.name for item in self.top_items if isinstance(item, FuncDecl) and item.is_method}
|
|
548
|
+
single = {function.name: function for function in declared
|
|
549
|
+
if function.name not in self.overloaded and function.name not in method_names}
|
|
550
|
+
calls = {name: [node for node in walk(function.body)
|
|
551
|
+
if isinstance(node, FunctionCall) and isinstance(node.func, str) and node.func in single]
|
|
552
|
+
for name, function in single.items()}
|
|
553
|
+
callees = {name: {call.func for call in found} for name, found in calls.items()}
|
|
554
|
+
for name, function in single.items():
|
|
555
|
+
closing = next((call for call in calls[name]
|
|
556
|
+
if call.func == name or self.reaches(call.func, name, callees)), None)
|
|
557
|
+
if closing is not None:
|
|
558
|
+
self.report_recursion(name, closing)
|
|
559
|
+
for function in declared:
|
|
560
|
+
if function.name in self.overloaded:
|
|
561
|
+
own = next((node for node in walk(function.body) if _passes_own_parameters(node, function)), None)
|
|
562
|
+
if own is not None:
|
|
563
|
+
self.report_recursion(function.name, own)
|
|
564
|
+
|
|
565
|
+
def report_recursion(self, name: str, call: FunctionCall) -> None:
|
|
566
|
+
self.report(REFUSE, 'recursion', name, f"function '{name}' calls itself (directly or through another "
|
|
567
|
+
f'function); TradingView does not allow recursion', call)
|
|
568
|
+
|
|
569
|
+
@staticmethod
|
|
570
|
+
def reaches(start: str, goal: str, callees: Dict[str, set]) -> bool:
|
|
571
|
+
seen, pending = set(), list(callees[start])
|
|
572
|
+
while pending:
|
|
573
|
+
name = pending.pop()
|
|
574
|
+
if name == goal:
|
|
575
|
+
return True
|
|
576
|
+
if name not in seen:
|
|
577
|
+
seen.add(name)
|
|
578
|
+
pending.extend(callees.get(name, ()))
|
|
579
|
+
return False
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _names_declared_in(nodes: list) -> set:
|
|
583
|
+
"""Every name a declaration, loop or parameter in `nodes` binds."""
|
|
584
|
+
names = {name for node in nodes for name in _declared_names(node)}
|
|
585
|
+
for node in nodes:
|
|
586
|
+
if isinstance(node, ForLoop):
|
|
587
|
+
names.add(node.var)
|
|
588
|
+
elif isinstance(node, ForInLoop):
|
|
589
|
+
names.update(node.vars)
|
|
590
|
+
elif isinstance(node, FuncDecl):
|
|
591
|
+
names.update(parameter.name for parameter in node.params)
|
|
592
|
+
return names
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _passes_own_parameters(node, function: FuncDecl) -> bool:
|
|
596
|
+
"""`f(x)` inside `f(float x)`: the call's arguments are the function's own
|
|
597
|
+
parameters, in order and unchanged (no local of their names), so it
|
|
598
|
+
selects this very overload."""
|
|
599
|
+
if not (isinstance(node, FunctionCall) and node.func == function.name and not node.kwargs):
|
|
600
|
+
return False
|
|
601
|
+
parameters = [parameter.name for parameter in function.params]
|
|
602
|
+
arguments = [argument.name if isinstance(argument, Identifier) else None for argument in node.args]
|
|
603
|
+
redeclared = _names_declared_in(list(walk(function.body)))
|
|
604
|
+
return arguments == parameters and not redeclared.intersection(parameters)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Application layer: the transpile use case and the stages it is built from."""
|
|
2
|
+
from .service import TranspileService
|
|
3
|
+
from .stages import (
|
|
4
|
+
CommentOnlySourceFilter,
|
|
5
|
+
FreshTransformer,
|
|
6
|
+
LegacyFunctionNameNormalizer,
|
|
7
|
+
PineSemanticGuard,
|
|
8
|
+
RepairingSourceParser,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'CommentOnlySourceFilter',
|
|
13
|
+
'FreshTransformer',
|
|
14
|
+
'LegacyFunctionNameNormalizer',
|
|
15
|
+
'PineSemanticGuard',
|
|
16
|
+
'RepairingSourceParser',
|
|
17
|
+
'TranspileService',
|
|
18
|
+
]
|