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,398 @@
|
|
|
1
|
+
"""Parse-failure recovery (card #55).
|
|
2
|
+
|
|
3
|
+
The lexer and the grammar are unchanged. Only when the lexer or the parser
|
|
4
|
+
REJECTS a source does this module look at what was rejected. If it is one of
|
|
5
|
+
the spellings of valid Pine below, it is rewritten into the spelling the grammar
|
|
6
|
+
already accepts, and the source is parsed again. A source that parses today
|
|
7
|
+
never reaches a repair, so its output cannot change.
|
|
8
|
+
|
|
9
|
+
row 1 `\\xa0` used as whitespace -> a space (outside strings and comments)
|
|
10
|
+
CRLF `\\r\\n` / `\\r` line ends in a str -> `\\n`, as reading the file in text mode does
|
|
11
|
+
row 2 unary `+x` -> `x`
|
|
12
|
+
row 3 `.99` -> `0.99`
|
|
13
|
+
row 4 `a = 1, b = 2` at the top level -> two statements, only when a statement
|
|
14
|
+
(assignment, declaration or call) follows the comma
|
|
15
|
+
row 5 a wrapped line led by an operator, on an indent no block claims -> joined
|
|
16
|
+
row 8 `0 => k := k + 1` in an expression switch -> the multi-line arm (any assignment
|
|
17
|
+
right after `=>` the parser rejects, e.g. a one-line function body, gets the
|
|
18
|
+
same multi-line form)
|
|
19
|
+
|
|
20
|
+
Rows 2, 3 and 4 are repaired at every site of the kind in one pass (G6): no site
|
|
21
|
+
they match can parse as written, so rewriting one the parser has not reached yet
|
|
22
|
+
changes nothing it would have accepted. Rows 5 and 8 depend on which INDENT a
|
|
23
|
+
block claims, which only the parser knows, so they are repaired one site per parse.
|
|
24
|
+
Each parse costs one pass of the file's budget; when a chain dead-ends or the
|
|
25
|
+
budget runs out, the FIRST error is raised, with the message and line:col the
|
|
26
|
+
unrepaired source gave, and the error the chain ended on as its __cause__.
|
|
27
|
+
|
|
28
|
+
UNPROVEN on TradingView, pinned by tests/test_parse_repair_card55.py: `\\xa0` as
|
|
29
|
+
whitespace; comma statements; `var p = 0, q = 0` makes only `p` a var here; a
|
|
30
|
+
statement led by `+` on the indent grid stays refused; unary `+` on a non-number
|
|
31
|
+
(`+"a"`) is accepted; an operator-led wrap indented by a multiple of 4 is joined, as
|
|
32
|
+
the lexer already joins a trailing-operator wrap on any indent.
|
|
33
|
+
"""
|
|
34
|
+
from typing import List, Optional, Set, Tuple
|
|
35
|
+
|
|
36
|
+
from .ast_nodes import Script
|
|
37
|
+
from .errors import LexerError, ParserError
|
|
38
|
+
from .lexer import Lexer
|
|
39
|
+
from .parser import Parser
|
|
40
|
+
from .tokens import Token, TokenType
|
|
41
|
+
|
|
42
|
+
MINIMUM_REPAIR_PASSES = 200
|
|
43
|
+
NO_BREAK_SPACE = '\xa0'
|
|
44
|
+
CARRIAGE_RETURN = '\r'
|
|
45
|
+
|
|
46
|
+
OPERAND_END_TYPES = {
|
|
47
|
+
TokenType.INT_LITERAL, TokenType.FLOAT_LITERAL, TokenType.STRING_LITERAL,
|
|
48
|
+
TokenType.BOOL_LITERAL, TokenType.COLOR_LITERAL, TokenType.NA_LITERAL,
|
|
49
|
+
TokenType.IDENTIFIER, TokenType.TYPE_IDENTIFIER, TokenType.TRUE, TokenType.FALSE,
|
|
50
|
+
TokenType.RPAREN, TokenType.RBRACKET,
|
|
51
|
+
}
|
|
52
|
+
LINE_START_TYPES = {TokenType.NEWLINE, TokenType.INDENT, TokenType.DEDENT}
|
|
53
|
+
LINE_END_TYPES = LINE_START_TYPES | {TokenType.EOF}
|
|
54
|
+
OPENING_TYPES = {TokenType.LPAREN, TokenType.LBRACKET}
|
|
55
|
+
CLOSING_TYPES = {TokenType.RPAREN, TokenType.RBRACKET}
|
|
56
|
+
CONTINUATION_LEAD_TYPES = {
|
|
57
|
+
TokenType.PLUS, TokenType.MINUS, TokenType.MULT, TokenType.DIV, TokenType.MOD,
|
|
58
|
+
TokenType.TERNARY, TokenType.COLON, TokenType.AND, TokenType.OR,
|
|
59
|
+
TokenType.GT, TokenType.LT, TokenType.GTE, TokenType.LTE, TokenType.EQ, TokenType.NEQ,
|
|
60
|
+
TokenType.ASSIGN, TokenType.REASSIGN,
|
|
61
|
+
}
|
|
62
|
+
ASSIGNMENT_TYPES = {
|
|
63
|
+
TokenType.ASSIGN, TokenType.REASSIGN, TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN,
|
|
64
|
+
TokenType.MULT_ASSIGN, TokenType.DIV_ASSIGN, TokenType.MOD_ASSIGN,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_source(source: str) -> Tuple[Script, List[str]]:
|
|
69
|
+
"""Lex and parse `source`; return (Script, repairs), one 'kind@line:column' per repair pass."""
|
|
70
|
+
repairs: List[str] = []
|
|
71
|
+
tokens, first_lexer_error = _tokenize(source, repairs)
|
|
72
|
+
try:
|
|
73
|
+
return parse_tokens(tokens, repairs), repairs
|
|
74
|
+
except ParserError as error:
|
|
75
|
+
if first_lexer_error is None:
|
|
76
|
+
raise
|
|
77
|
+
raise first_lexer_error from error # the unrepaired source's first error
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def parse_tokens(tokens: List[Token], repairs: Optional[List[str]] = None) -> Script:
|
|
81
|
+
"""Parse `tokens`, repairing each rejected known spelling; append to `repairs` per pass."""
|
|
82
|
+
repairs = [] if repairs is None else repairs
|
|
83
|
+
first_error, budget, passes = None, None, 0
|
|
84
|
+
while True:
|
|
85
|
+
parser = Parser(tokens)
|
|
86
|
+
try:
|
|
87
|
+
return parser.parse()
|
|
88
|
+
except ParserError as error:
|
|
89
|
+
if first_error is None: # sized only once a source is refused
|
|
90
|
+
first_error, budget = error, repair_pass_budget(parser.tokens)
|
|
91
|
+
repaired = _repair(parser.tokens, error)
|
|
92
|
+
if repaired is None:
|
|
93
|
+
raise first_error from (None if error is first_error else error)
|
|
94
|
+
if passes == budget:
|
|
95
|
+
raise _budget_refusal(first_error, budget) from error
|
|
96
|
+
tokens, kind = repaired
|
|
97
|
+
repairs.append(f'{kind}@{error.line}:{error.column}')
|
|
98
|
+
passes += 1
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def repair_pass_budget(tokens: List[Token]) -> int:
|
|
102
|
+
"""Parses a source may spend on repairs, sized to its candidate sites (G6).
|
|
103
|
+
|
|
104
|
+
Each row 5 or row 8 repair fixes one candidate per parse, and each may expose new
|
|
105
|
+
sites for every batched kind, so a source with n candidates needs at most
|
|
106
|
+
n * (1 + len(BATCHED_REPAIRS)) passes beyond the batched kinds' own.
|
|
107
|
+
"""
|
|
108
|
+
context = _LineContext(tokens)
|
|
109
|
+
wraps = sum(1 for index in range(1, len(tokens) - 1)
|
|
110
|
+
if tokens[index].type == TokenType.INDENT and tokens[index - 1].type == TokenType.NEWLINE
|
|
111
|
+
and tokens[index + 1].type in CONTINUATION_LEAD_TYPES)
|
|
112
|
+
arm_assignments = sum(1 for index, token in enumerate(tokens)
|
|
113
|
+
if token.type in ASSIGNMENT_TYPES and context.has_arrow(index))
|
|
114
|
+
return MINIMUM_REPAIR_PASSES + (wraps + arm_assignments) * (1 + len(BATCHED_REPAIRS))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _budget_refusal(first_error: ParserError, budget: int) -> ParserError:
|
|
118
|
+
"""The first error, naming the repair budget the source ran out of."""
|
|
119
|
+
refusal = ParserError(f'{first_error.message} (still refused after {budget} repair passes, '
|
|
120
|
+
f'the budget for this file)', first_error.line, first_error.column)
|
|
121
|
+
refusal.token_index = getattr(first_error, 'token_index', None)
|
|
122
|
+
return refusal
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _tokenize(source: str, repairs: List[str]) -> Tuple[List[Token], Optional[LexerError]]:
|
|
126
|
+
"""Lex `source`; on a no-break space or a carriage return, rewrite it once and lex again.
|
|
127
|
+
|
|
128
|
+
Returns the tokens and the first lexer error a rewrite got past (None if none did).
|
|
129
|
+
"""
|
|
130
|
+
rewrites = {NO_BREAK_SPACE: ('no_break_space', _no_break_space_outside_strings),
|
|
131
|
+
CARRIAGE_RETURN: ('carriage_return', _universal_newlines)}
|
|
132
|
+
first_error = None
|
|
133
|
+
while True:
|
|
134
|
+
try:
|
|
135
|
+
return Lexer(source).tokenize(), first_error
|
|
136
|
+
except LexerError as error:
|
|
137
|
+
first_error = first_error or error
|
|
138
|
+
character = _rejected_character(error)
|
|
139
|
+
if character not in rewrites:
|
|
140
|
+
raise first_error from (None if error is first_error else error)
|
|
141
|
+
kind, rewrite = rewrites.pop(character)
|
|
142
|
+
repairs.append(f'{kind}@{error.line}:{error.column}')
|
|
143
|
+
source = rewrite(source)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _rejected_character(error: LexerError) -> Optional[str]:
|
|
147
|
+
"""The character of an `Unexpected character: <repr>` lexer error, else None."""
|
|
148
|
+
for character in (NO_BREAK_SPACE, CARRIAGE_RETURN):
|
|
149
|
+
if error.message == f'Unexpected character: {character!r}':
|
|
150
|
+
return character
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _universal_newlines(source: str) -> str:
|
|
155
|
+
"""`\\r\\n` and a lone `\\r` become `\\n`, as `Path.read_text` does for the CLI."""
|
|
156
|
+
return source.replace('\r\n', '\n').replace('\r', '\n')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _no_break_space_outside_strings(source: str) -> str:
|
|
160
|
+
"""Every `\\xa0` outside a string literal or a `//` comment becomes a space."""
|
|
161
|
+
characters, quote, index = list(source), None, 0
|
|
162
|
+
while index < len(characters):
|
|
163
|
+
character = characters[index]
|
|
164
|
+
if quote:
|
|
165
|
+
if character == '\\':
|
|
166
|
+
index += 1
|
|
167
|
+
elif character == quote: # the lexer lets a string span lines
|
|
168
|
+
quote = None
|
|
169
|
+
elif character in ('"', "'"):
|
|
170
|
+
quote = character
|
|
171
|
+
elif character == '/' and source[index:index + 2] == '//':
|
|
172
|
+
index = source.find('\n', index)
|
|
173
|
+
index = len(source) if index < 0 else index
|
|
174
|
+
continue
|
|
175
|
+
elif character == NO_BREAK_SPACE:
|
|
176
|
+
characters[index] = ' '
|
|
177
|
+
index += 1
|
|
178
|
+
return ''.join(characters)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _repair(tokens: List[Token], error: ParserError) -> Optional[Tuple[List[Token], str]]:
|
|
182
|
+
"""(rewritten tokens, kind) for the known spelling at the error's token, else None."""
|
|
183
|
+
index = getattr(error, 'token_index', None)
|
|
184
|
+
if index is None or not 0 <= index < len(tokens):
|
|
185
|
+
return None
|
|
186
|
+
context = _LineContext(tokens)
|
|
187
|
+
for kind, is_site, rewrite_site in BATCHED_REPAIRS:
|
|
188
|
+
if is_site(tokens, index, context):
|
|
189
|
+
sites = {position for position in range(len(tokens)) if is_site(tokens, position, context)}
|
|
190
|
+
return _rewrite_sites(tokens, sites, rewrite_site), kind
|
|
191
|
+
for repair in (_operator_led_wrap, _switch_arm_statement):
|
|
192
|
+
result = repair(tokens, index)
|
|
193
|
+
if result is not None:
|
|
194
|
+
return result, repair.__name__.lstrip('_')
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class _LineContext:
|
|
199
|
+
"""Per token: the bracket depth within its line, the block nesting, and whether its line has `=>`."""
|
|
200
|
+
|
|
201
|
+
def __init__(self, tokens: List[Token]):
|
|
202
|
+
self.depth: List[int] = []
|
|
203
|
+
self.nesting: List[int] = []
|
|
204
|
+
self.line: List[int] = []
|
|
205
|
+
self.lines_with_arrow: Set[int] = set()
|
|
206
|
+
depth = nesting = line = 0
|
|
207
|
+
for token in tokens:
|
|
208
|
+
if token.type in LINE_START_TYPES:
|
|
209
|
+
depth, line = 0, line + 1
|
|
210
|
+
nesting += (token.type == TokenType.INDENT) - (token.type == TokenType.DEDENT)
|
|
211
|
+
self.depth.append(depth)
|
|
212
|
+
self.nesting.append(nesting)
|
|
213
|
+
self.line.append(line)
|
|
214
|
+
depth += (token.type in OPENING_TYPES) - (token.type in CLOSING_TYPES)
|
|
215
|
+
if token.type == TokenType.ARROW:
|
|
216
|
+
self.lines_with_arrow.add(line)
|
|
217
|
+
|
|
218
|
+
def has_arrow(self, index: int) -> bool:
|
|
219
|
+
return self.line[index] in self.lines_with_arrow
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _rewrite_sites(tokens: List[Token], sites: Set[int], rewrite_site) -> List[Token]:
|
|
223
|
+
"""A new token list with `rewrite_site(tokens, index)` in place of each site's token."""
|
|
224
|
+
output: List[Token] = []
|
|
225
|
+
index = 0
|
|
226
|
+
while index < len(tokens):
|
|
227
|
+
if index in sites:
|
|
228
|
+
replacement, consumed = rewrite_site(tokens, index)
|
|
229
|
+
output.extend(replacement)
|
|
230
|
+
index += consumed
|
|
231
|
+
else:
|
|
232
|
+
output.append(tokens[index])
|
|
233
|
+
index += 1
|
|
234
|
+
return output
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _is_unary_plus(tokens: List[Token], index: int, context: _LineContext) -> bool:
|
|
238
|
+
"""Row 2: a `+` with no left operand. At a line start only on a `=>` line (`+1 => a`)."""
|
|
239
|
+
if tokens[index].type != TokenType.PLUS or index == 0:
|
|
240
|
+
return False
|
|
241
|
+
previous = tokens[index - 1].type
|
|
242
|
+
if previous in OPERAND_END_TYPES:
|
|
243
|
+
return False # a binary `+`, e.g. `(a) + +b`
|
|
244
|
+
return previous not in LINE_START_TYPES or context.has_arrow(index)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _drop_token(tokens: List[Token], index: int) -> Tuple[List[Token], int]:
|
|
248
|
+
return [], 1
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _is_leading_dot_number(tokens: List[Token], index: int, context: _LineContext) -> bool:
|
|
252
|
+
"""Row 3: `.` written right against the digits of a number (`. 5` stays refused)."""
|
|
253
|
+
dot = tokens[index]
|
|
254
|
+
number = tokens[index + 1] if index + 1 < len(tokens) else None
|
|
255
|
+
return (dot.type == TokenType.DOT and number is not None
|
|
256
|
+
and number.type in (TokenType.INT_LITERAL, TokenType.FLOAT_LITERAL)
|
|
257
|
+
and (number.line, number.column) == (dot.line, dot.column + 1)
|
|
258
|
+
and (number.lexeme or '')[:1].isdigit() and '.' not in number.lexeme)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _merge_leading_dot_number(tokens: List[Token], index: int) -> Tuple[List[Token], int]:
|
|
262
|
+
dot, number = tokens[index], tokens[index + 1]
|
|
263
|
+
lexeme = '0.' + number.lexeme
|
|
264
|
+
return [Token(TokenType.FLOAT_LITERAL, float(lexeme), dot.line, dot.column, lexeme)], 2
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _is_comma_statement(tokens: List[Token], index: int, context: _LineContext) -> bool:
|
|
268
|
+
"""Row 4: a comma between two top-level statements, outside brackets and `=>` lines (G3).
|
|
269
|
+
|
|
270
|
+
Both sides must be a statement, so the comma of `map<string, float> m = ...` (bracket
|
|
271
|
+
depth 0, but inside `<...>`) is never a site: `map<string` is not a statement; nor is
|
|
272
|
+
a comma at a line start or after another comma, whose item before it is empty. A
|
|
273
|
+
comma ending its line joins the statement that starts the next line at the same indent.
|
|
274
|
+
"""
|
|
275
|
+
if (tokens[index].type != TokenType.COMMA or context.depth[index] != 0
|
|
276
|
+
or context.nesting[index] != 0 or context.has_arrow(index)):
|
|
277
|
+
return False
|
|
278
|
+
after = _item_after_comma(tokens, index)
|
|
279
|
+
return (_is_statement_item(tokens, _item_start(tokens, index, context), index)
|
|
280
|
+
and _is_statement_item(tokens, after, _item_end(tokens, after)))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _item_after_comma(tokens: List[Token], comma: int) -> int:
|
|
284
|
+
"""First token of the item after `comma`; for `a = 1,` ending its line, the next line's first."""
|
|
285
|
+
position = comma + 1
|
|
286
|
+
while position < len(tokens) and tokens[position].type == TokenType.NEWLINE:
|
|
287
|
+
position += 1
|
|
288
|
+
return position
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _item_start(tokens: List[Token], comma: int, context: _LineContext) -> int:
|
|
292
|
+
"""First token of the item before `comma`: after the line start or the previous top-level comma."""
|
|
293
|
+
position = comma - 1
|
|
294
|
+
while position >= 0 and tokens[position].type not in LINE_START_TYPES and not (
|
|
295
|
+
tokens[position].type == TokenType.COMMA and context.depth[position] == 0):
|
|
296
|
+
position -= 1
|
|
297
|
+
return position + 1
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _item_end(tokens: List[Token], start: int) -> int:
|
|
301
|
+
"""Index after the item from `start`: the next top-level comma or the line end."""
|
|
302
|
+
depth, position = 0, start
|
|
303
|
+
while position < len(tokens):
|
|
304
|
+
kind = tokens[position].type
|
|
305
|
+
if kind in LINE_END_TYPES or (kind == TokenType.COMMA and depth == 0):
|
|
306
|
+
break
|
|
307
|
+
depth += (kind in OPENING_TYPES) - (kind in CLOSING_TYPES)
|
|
308
|
+
position += 1
|
|
309
|
+
return position
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _is_statement_item(tokens: List[Token], start: int, end: int) -> bool:
|
|
313
|
+
"""tokens[start:end] is an assignment or declaration (`=` at its top level), or a call."""
|
|
314
|
+
depth = 0
|
|
315
|
+
for position in range(start, end):
|
|
316
|
+
kind = tokens[position].type
|
|
317
|
+
if depth == 0 and kind in ASSIGNMENT_TYPES:
|
|
318
|
+
return True # `b = 2`, `var float b = 2`, `[p, q] = f()`
|
|
319
|
+
depth += (kind in OPENING_TYPES) - (kind in CLOSING_TYPES)
|
|
320
|
+
return _is_call_item(tokens, start, end)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _is_call_item(tokens: List[Token], start: int, end: int) -> bool:
|
|
324
|
+
"""tokens[start:end] is exactly `name(...)`, `name.method(...)` or `name .method(...)`."""
|
|
325
|
+
if start >= end or tokens[start].type != TokenType.IDENTIFIER:
|
|
326
|
+
return False
|
|
327
|
+
paren = start + 1
|
|
328
|
+
while paren + 1 < end and tokens[paren].type == TokenType.DOT and tokens[paren + 1].type == TokenType.IDENTIFIER:
|
|
329
|
+
paren += 2 # `pivots .append(...)`: a spaced member call
|
|
330
|
+
if paren >= end or tokens[paren].type != TokenType.LPAREN:
|
|
331
|
+
return False
|
|
332
|
+
depth = 0
|
|
333
|
+
for position in range(paren, end):
|
|
334
|
+
depth += (tokens[position].type in OPENING_TYPES) - (tokens[position].type in CLOSING_TYPES)
|
|
335
|
+
if depth == 0:
|
|
336
|
+
return position == end - 1
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _comma_to_newline(tokens: List[Token], index: int) -> Tuple[List[Token], int]:
|
|
341
|
+
comma = tokens[index]
|
|
342
|
+
return [Token(TokenType.NEWLINE, '\n', comma.line, comma.column)], 1
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
BATCHED_REPAIRS = (
|
|
346
|
+
('unary_plus', _is_unary_plus, _drop_token),
|
|
347
|
+
('leading_dot_number', _is_leading_dot_number, _merge_leading_dot_number),
|
|
348
|
+
('comma_statement', _is_comma_statement, _comma_to_newline),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _operator_led_wrap(tokens: List[Token], index: int) -> Optional[List[Token]]:
|
|
353
|
+
"""Row 5: an INDENT no block claimed, before an operator: join the wrapped lines."""
|
|
354
|
+
if tokens[index].type == TokenType.NEWLINE and index + 1 < len(tokens):
|
|
355
|
+
index += 1 # `Expected COLON, got NEWLINE` names the NEWLINE
|
|
356
|
+
if not (tokens[index].type == TokenType.INDENT and index >= 1
|
|
357
|
+
and tokens[index - 1].type == TokenType.NEWLINE and index + 1 < len(tokens)
|
|
358
|
+
and tokens[index + 1].type in CONTINUATION_LEAD_TYPES):
|
|
359
|
+
return None
|
|
360
|
+
output = tokens[:index - 1]
|
|
361
|
+
position = index + 1
|
|
362
|
+
while position < len(tokens):
|
|
363
|
+
token = tokens[position]
|
|
364
|
+
if token.type == TokenType.INDENT:
|
|
365
|
+
return None # a nested indent under a wrap: not handled
|
|
366
|
+
if token.type == TokenType.DEDENT:
|
|
367
|
+
return output + tokens[position + 1:] # the wrap closes at EOF: drop its DEDENT
|
|
368
|
+
if token.type == TokenType.NEWLINE:
|
|
369
|
+
following = tokens[position + 1] if position + 1 < len(tokens) else None
|
|
370
|
+
if following is not None and following.type in CONTINUATION_LEAD_TYPES:
|
|
371
|
+
position += 1 # another wrapped line at the same indent
|
|
372
|
+
continue
|
|
373
|
+
if following is not None and following.type == TokenType.DEDENT:
|
|
374
|
+
return output + [token] + tokens[position + 2:] # drop the matching DEDENT
|
|
375
|
+
return None # a non-operator line at the wrap indent
|
|
376
|
+
output.append(token)
|
|
377
|
+
position += 1
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _switch_arm_statement(tokens: List[Token], index: int) -> Optional[List[Token]]:
|
|
382
|
+
"""Row 8: an assignment on a `=>` arm line becomes the arm's indented body."""
|
|
383
|
+
if tokens[index].type not in ASSIGNMENT_TYPES:
|
|
384
|
+
return None
|
|
385
|
+
arrow = index - 1
|
|
386
|
+
while arrow >= 0 and tokens[arrow].type not in LINE_START_TYPES | {TokenType.ARROW}:
|
|
387
|
+
arrow -= 1
|
|
388
|
+
if arrow < 0 or tokens[arrow].type != TokenType.ARROW:
|
|
389
|
+
return None
|
|
390
|
+
end = index
|
|
391
|
+
while end < len(tokens) and tokens[end].type not in (TokenType.NEWLINE, TokenType.EOF):
|
|
392
|
+
end += 1
|
|
393
|
+
near = tokens[arrow]
|
|
394
|
+
newline, indent, dedent = (Token(kind, '\n' if kind == TokenType.NEWLINE else None, near.line, near.column)
|
|
395
|
+
for kind in (TokenType.NEWLINE, TokenType.INDENT, TokenType.DEDENT))
|
|
396
|
+
line_end = tokens[end:end + 1] if tokens[end].type == TokenType.NEWLINE else [newline]
|
|
397
|
+
rest = tokens[end + 1:] if tokens[end].type == TokenType.NEWLINE else tokens[end:]
|
|
398
|
+
return tokens[:arrow + 1] + [newline, indent] + tokens[arrow + 1:end] + line_end + [dedent] + rest
|