shell-lite 0.4.2__py3-none-any.whl → 0.4.3__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.
- shell_lite/fix_nulls.py +29 -0
- shell_lite/interpreter.py +74 -5
- shell_lite/interpreter_backup.py +1781 -0
- shell_lite/interpreter_final.py +1773 -0
- shell_lite/interpreter_new.py +1773 -0
- shell_lite/lexer.py +10 -3
- shell_lite/lexer_new.py +252 -0
- shell_lite/main.py +16 -5
- shell_lite/minimal_interpreter.py +25 -0
- shell_lite/parser.py +279 -2
- shell_lite/parser_new.py +2229 -0
- shell_lite/patch_parser.py +41 -0
- {shell_lite-0.4.2.dist-info → shell_lite-0.4.3.dist-info}/METADATA +1 -1
- shell_lite-0.4.3.dist-info/RECORD +25 -0
- shell_lite-0.4.2.dist-info/RECORD +0 -17
- {shell_lite-0.4.2.dist-info → shell_lite-0.4.3.dist-info}/LICENSE +0 -0
- {shell_lite-0.4.2.dist-info → shell_lite-0.4.3.dist-info}/WHEEL +0 -0
- {shell_lite-0.4.2.dist-info → shell_lite-0.4.3.dist-info}/entry_points.txt +0 -0
- {shell_lite-0.4.2.dist-info → shell_lite-0.4.3.dist-info}/top_level.txt +0 -0
shell_lite/parser_new.py
ADDED
|
@@ -0,0 +1,2229 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from .lexer_new import Token, Lexer
|
|
3
|
+
from .ast_nodes import *
|
|
4
|
+
import re
|
|
5
|
+
class Parser:
|
|
6
|
+
def __init__(self, tokens: List[Token]):
|
|
7
|
+
self.tokens = [t for t in tokens if t.type != 'COMMENT']
|
|
8
|
+
self.pos = 0
|
|
9
|
+
def peek(self, offset: int = 0) -> Token:
|
|
10
|
+
if self.pos + offset < len(self.tokens):
|
|
11
|
+
return self.tokens[self.pos + offset]
|
|
12
|
+
return self.tokens[-1]
|
|
13
|
+
def consume(self, expected_type: str = None) -> Token:
|
|
14
|
+
token = self.peek()
|
|
15
|
+
if expected_type and token.type != expected_type:
|
|
16
|
+
raise SyntaxError(f"Expected {expected_type} but got {token.type} on line {token.line}")
|
|
17
|
+
self.pos += 1
|
|
18
|
+
return token
|
|
19
|
+
def check(self, token_type: str) -> bool:
|
|
20
|
+
return self.peek().type == token_type
|
|
21
|
+
def parse(self) -> List[Node]:
|
|
22
|
+
statements = []
|
|
23
|
+
while not self.check('EOF'):
|
|
24
|
+
while self.check('NEWLINE'):
|
|
25
|
+
self.consume()
|
|
26
|
+
if self.check('EOF'): break
|
|
27
|
+
if self.check('EOF'): break
|
|
28
|
+
stmt = self.parse_statement()
|
|
29
|
+
if stmt:
|
|
30
|
+
statements.append(stmt)
|
|
31
|
+
return statements
|
|
32
|
+
def parse_statement(self) -> Node:
|
|
33
|
+
if self.check('USE'):
|
|
34
|
+
return self.parse_import()
|
|
35
|
+
elif self.check('APP'):
|
|
36
|
+
return self.parse_app()
|
|
37
|
+
elif self.check('ON'):
|
|
38
|
+
return self.parse_on()
|
|
39
|
+
elif self.check('CONST'):
|
|
40
|
+
return self.parse_const()
|
|
41
|
+
elif self.check('PRINT') or self.check('SAY'):
|
|
42
|
+
return self.parse_print()
|
|
43
|
+
elif self.check('ALERT'):
|
|
44
|
+
return self.parse_alert()
|
|
45
|
+
elif self.check('IF'):
|
|
46
|
+
return self.parse_if()
|
|
47
|
+
elif self.check('UNLESS'):
|
|
48
|
+
return self.parse_unless()
|
|
49
|
+
elif self.check('WHILE'):
|
|
50
|
+
return self.parse_while()
|
|
51
|
+
elif self.check('UNTIL'):
|
|
52
|
+
return self.parse_until()
|
|
53
|
+
elif self.check('FOREVER'):
|
|
54
|
+
return self.parse_forever()
|
|
55
|
+
elif self.check('TRY'):
|
|
56
|
+
return self.parse_try()
|
|
57
|
+
elif self.check('FOR') or self.check('LOOP'):
|
|
58
|
+
return self.parse_for()
|
|
59
|
+
elif self.check('REPEAT'):
|
|
60
|
+
return self.parse_repeat()
|
|
61
|
+
elif self.check('WHEN'):
|
|
62
|
+
return self.parse_when()
|
|
63
|
+
elif self.check('TO'):
|
|
64
|
+
return self.parse_function_def()
|
|
65
|
+
elif self.check('STRUCTURE'):
|
|
66
|
+
return self.parse_class_def()
|
|
67
|
+
elif self.check('RETURN'):
|
|
68
|
+
return self.parse_return()
|
|
69
|
+
elif self.check('STOP'):
|
|
70
|
+
return self.parse_stop()
|
|
71
|
+
elif self.check('SKIP'):
|
|
72
|
+
return self.parse_skip()
|
|
73
|
+
elif self.check('EXIT'):
|
|
74
|
+
return self.parse_exit()
|
|
75
|
+
elif self.check('ERROR'):
|
|
76
|
+
return self.parse_error()
|
|
77
|
+
elif self.check('EXECUTE'):
|
|
78
|
+
return self.parse_execute()
|
|
79
|
+
elif self.check('MAKE'):
|
|
80
|
+
return self.parse_make()
|
|
81
|
+
elif self.check('INPUT'):
|
|
82
|
+
next_t = self.peek(1)
|
|
83
|
+
if next_t.type in ('ID', 'TYPE', 'STRING', 'NAME', 'VALUE', 'CLASS', 'STYLE', 'ONCLICK', 'SRC', 'HREF', 'ACTION', 'METHOD'):
|
|
84
|
+
input_token = self.consume()
|
|
85
|
+
return self.parse_id_start_statement(passed_name_token=input_token)
|
|
86
|
+
return self.parse_expression_stmt()
|
|
87
|
+
elif self.check('ID'):
|
|
88
|
+
return self.parse_id_start_statement()
|
|
89
|
+
elif self.check('SPAWN'):
|
|
90
|
+
expr = self.parse_expression()
|
|
91
|
+
self.consume('NEWLINE')
|
|
92
|
+
return Print(expr)
|
|
93
|
+
pass
|
|
94
|
+
elif self.check('WAIT'):
|
|
95
|
+
return self.parse_wait()
|
|
96
|
+
elif self.check('EVERY'):
|
|
97
|
+
return self.parse_every()
|
|
98
|
+
elif self.check('IN'):
|
|
99
|
+
return self.parse_after()
|
|
100
|
+
elif self.check('LISTEN'):
|
|
101
|
+
return self.parse_listen()
|
|
102
|
+
elif self.check('SERVE'):
|
|
103
|
+
return self.parse_serve()
|
|
104
|
+
elif self.check('DOWNLOAD'):
|
|
105
|
+
return self.parse_download()
|
|
106
|
+
elif self.check('COMPRESS') or self.check('EXTRACT'):
|
|
107
|
+
return self.parse_archive()
|
|
108
|
+
elif self.check('LOAD') or self.check('SAVE'):
|
|
109
|
+
if self.check('LOAD') and self.peek(1).type == 'CSV':
|
|
110
|
+
return self.parse_csv_load()
|
|
111
|
+
if self.check('SAVE'):
|
|
112
|
+
return self.parse_csv_save()
|
|
113
|
+
return self.parse_expression_stmt()
|
|
114
|
+
elif self.check('COPY') or self.check('PASTE'):
|
|
115
|
+
return self.parse_clipboard()
|
|
116
|
+
elif self.check('WRITE'):
|
|
117
|
+
return self.parse_write()
|
|
118
|
+
elif self.check('APPEND'):
|
|
119
|
+
return self.parse_append()
|
|
120
|
+
elif self.check('DB'):
|
|
121
|
+
return self.parse_db_op()
|
|
122
|
+
elif self.check('PRESS') or self.check('TYPE') or self.check('CLICK') or self.check('NOTIFY'):
|
|
123
|
+
return self.parse_automation()
|
|
124
|
+
elif self.check('BEFORE'):
|
|
125
|
+
return self.parse_middleware()
|
|
126
|
+
elif self.check('DEFINE'):
|
|
127
|
+
if self.peek(1).type == 'FUNCTION':
|
|
128
|
+
return self.parse_function_def()
|
|
129
|
+
return self.parse_define_page()
|
|
130
|
+
elif self.check('ADD'):
|
|
131
|
+
if self.peek(1).type == 'ID' or self.peek(1).type == 'NUMBER' or self.peek(1).type == 'STRING':
|
|
132
|
+
# Heuristic: "Add X to Y" (List) vs "Add Component" (UI) maybe?
|
|
133
|
+
# Assuming UI "add" uses 'ADD' keyword?
|
|
134
|
+
# Lexer maps 'add' to 'ADD'.
|
|
135
|
+
# parse_add_to -> UI.
|
|
136
|
+
# parse_add_to_list -> List.
|
|
137
|
+
# Let's check parse_add_to signature.
|
|
138
|
+
pass
|
|
139
|
+
# For now prioritize List if 'TO' is present later?
|
|
140
|
+
# Or assume parse_add_to handles UI.
|
|
141
|
+
# Let's peek(1). If it's an expression -> List op. If it's a Component?
|
|
142
|
+
# Creating a unified parse_add dispatcher might be better.
|
|
143
|
+
return self.parse_add_distinguish()
|
|
144
|
+
|
|
145
|
+
elif self.check('START'):
|
|
146
|
+
return self.parse_start_server()
|
|
147
|
+
elif self.check('HEADING'):
|
|
148
|
+
return self.parse_heading()
|
|
149
|
+
elif self.check('PARAGRAPH'):
|
|
150
|
+
return self.parse_paragraph()
|
|
151
|
+
|
|
152
|
+
# English List/Time Ops
|
|
153
|
+
if self.check('ADD'):
|
|
154
|
+
return self.parse_add_to_list()
|
|
155
|
+
if self.check('REMOVE'):
|
|
156
|
+
return self.parse_remove_from_list()
|
|
157
|
+
if self.check('WAIT'):
|
|
158
|
+
return self.parse_wait()
|
|
159
|
+
|
|
160
|
+
if self.check('INCREMENT'):
|
|
161
|
+
return self.parse_increment()
|
|
162
|
+
elif self.check('DECREMENT'):
|
|
163
|
+
return self.parse_decrement()
|
|
164
|
+
elif self.check('MULTIPLY'):
|
|
165
|
+
return self.parse_multiply()
|
|
166
|
+
elif self.check('DIVIDE'):
|
|
167
|
+
return self.parse_divide()
|
|
168
|
+
elif self.check('MAKE'):
|
|
169
|
+
return self.parse_make_assignment()
|
|
170
|
+
elif self.check('AS'):
|
|
171
|
+
return self.parse_as_long_as()
|
|
172
|
+
elif self.check('ASK'):
|
|
173
|
+
# Standalone ask statement? e.g. ask "Questions?"
|
|
174
|
+
# Or ask is expression. If statement, maybe just expression statement.
|
|
175
|
+
return self.parse_expression_statement()
|
|
176
|
+
elif self.check('SET'):
|
|
177
|
+
return self.parse_set()
|
|
178
|
+
else:
|
|
179
|
+
return self.parse_expression_stmt()
|
|
180
|
+
def parse_alert(self) -> Alert:
|
|
181
|
+
token = self.consume('ALERT')
|
|
182
|
+
message = self.parse_expression()
|
|
183
|
+
self.consume('NEWLINE')
|
|
184
|
+
node = Alert(message)
|
|
185
|
+
node.line = token.line
|
|
186
|
+
return node
|
|
187
|
+
def parse_const(self) -> ConstAssign:
|
|
188
|
+
token = self.consume('CONST')
|
|
189
|
+
name = self.consume('ID').value
|
|
190
|
+
self.consume('ASSIGN')
|
|
191
|
+
value = self.parse_expression()
|
|
192
|
+
self.consume('NEWLINE')
|
|
193
|
+
node = ConstAssign(name, value)
|
|
194
|
+
node.line = token.line
|
|
195
|
+
return node
|
|
196
|
+
def parse_on(self) -> Node:
|
|
197
|
+
token = self.consume('ON')
|
|
198
|
+
if self.check('REQUEST') or (self.check('ID') and self.peek().value == 'request'):
|
|
199
|
+
self.consume()
|
|
200
|
+
if self.check('TO'): self.consume('TO')
|
|
201
|
+
path = self.parse_expression()
|
|
202
|
+
self.consume('NEWLINE')
|
|
203
|
+
self.consume('INDENT')
|
|
204
|
+
body = []
|
|
205
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
206
|
+
while self.check('NEWLINE'): self.consume()
|
|
207
|
+
if self.check('DEDENT'): break
|
|
208
|
+
body.append(self.parse_statement())
|
|
209
|
+
self.consume('DEDENT')
|
|
210
|
+
node = OnRequest(path, body)
|
|
211
|
+
node.line = token.line
|
|
212
|
+
return node
|
|
213
|
+
event_type = self.consume('ID').value
|
|
214
|
+
path = self.parse_expression()
|
|
215
|
+
self.consume('NEWLINE')
|
|
216
|
+
self.consume('INDENT')
|
|
217
|
+
body = []
|
|
218
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
219
|
+
while self.check('NEWLINE'): self.consume()
|
|
220
|
+
if self.check('DEDENT'): break
|
|
221
|
+
body.append(self.parse_statement())
|
|
222
|
+
self.consume('DEDENT')
|
|
223
|
+
return FileWatcher(path, body)
|
|
224
|
+
def _parse_natural_list(self) -> ListVal:
|
|
225
|
+
self.consume('ID')
|
|
226
|
+
self.consume('LIST')
|
|
227
|
+
self.consume('OF')
|
|
228
|
+
elements = []
|
|
229
|
+
if not self.check('NEWLINE') and not self.check('EOF'):
|
|
230
|
+
elements.append(self.parse_expression())
|
|
231
|
+
while self.check('COMMA'):
|
|
232
|
+
self.consume('COMMA')
|
|
233
|
+
if self.check('NEWLINE'): break
|
|
234
|
+
elements.append(self.parse_expression())
|
|
235
|
+
node = ListVal(elements)
|
|
236
|
+
return node
|
|
237
|
+
def _parse_natural_set(self) -> Node:
|
|
238
|
+
self.consume('ID')
|
|
239
|
+
self.consume('UNIQUE')
|
|
240
|
+
self.consume('SET')
|
|
241
|
+
self.consume('OF')
|
|
242
|
+
elements = []
|
|
243
|
+
if not self.check('NEWLINE') and not self.check('EOF'):
|
|
244
|
+
elements.append(self.parse_expression())
|
|
245
|
+
while self.check('COMMA'):
|
|
246
|
+
self.consume('COMMA')
|
|
247
|
+
if self.check('NEWLINE'): break
|
|
248
|
+
elements.append(self.parse_expression())
|
|
249
|
+
list_node = ListVal(elements)
|
|
250
|
+
return Call('Set', [list_node])
|
|
251
|
+
def parse_wait(self) -> Node:
|
|
252
|
+
token = self.consume('WAIT')
|
|
253
|
+
if self.check('FOR'):
|
|
254
|
+
self.consume('FOR')
|
|
255
|
+
time_expr = self.parse_expression()
|
|
256
|
+
if self.check('SECOND'):
|
|
257
|
+
self.consume()
|
|
258
|
+
self.consume('NEWLINE')
|
|
259
|
+
return Call('wait', [time_expr])
|
|
260
|
+
def parse_stop(self) -> Stop:
|
|
261
|
+
token = self.consume('STOP')
|
|
262
|
+
self.consume('NEWLINE')
|
|
263
|
+
node = Stop()
|
|
264
|
+
node.line = token.line
|
|
265
|
+
return node
|
|
266
|
+
def parse_skip(self) -> Skip:
|
|
267
|
+
token = self.consume('SKIP')
|
|
268
|
+
self.consume('NEWLINE')
|
|
269
|
+
node = Skip()
|
|
270
|
+
node.line = token.line
|
|
271
|
+
return node
|
|
272
|
+
def parse_error(self) -> Throw:
|
|
273
|
+
token = self.consume('ERROR')
|
|
274
|
+
message = self.parse_expression()
|
|
275
|
+
self.consume('NEWLINE')
|
|
276
|
+
node = Throw(message)
|
|
277
|
+
node.line = token.line
|
|
278
|
+
return node
|
|
279
|
+
def parse_execute(self) -> Execute:
|
|
280
|
+
token = self.consume('EXECUTE')
|
|
281
|
+
code = self.parse_expression()
|
|
282
|
+
self.consume('NEWLINE')
|
|
283
|
+
node = Execute(code)
|
|
284
|
+
node.line = token.line
|
|
285
|
+
return node
|
|
286
|
+
def parse_unless(self) -> Unless:
|
|
287
|
+
token = self.consume('UNLESS')
|
|
288
|
+
condition = self.parse_expression()
|
|
289
|
+
self.consume('NEWLINE')
|
|
290
|
+
self.consume('INDENT')
|
|
291
|
+
body = []
|
|
292
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
293
|
+
while self.check('NEWLINE'): self.consume()
|
|
294
|
+
if self.check('DEDENT'): break
|
|
295
|
+
body.append(self.parse_statement())
|
|
296
|
+
self.consume('DEDENT')
|
|
297
|
+
else_body = None
|
|
298
|
+
if self.check('ELSE'):
|
|
299
|
+
self.consume('ELSE')
|
|
300
|
+
self.consume('NEWLINE')
|
|
301
|
+
self.consume('INDENT')
|
|
302
|
+
else_body = []
|
|
303
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
304
|
+
while self.check('NEWLINE'): self.consume()
|
|
305
|
+
if self.check('DEDENT'): break
|
|
306
|
+
else_body.append(self.parse_statement())
|
|
307
|
+
self.consume('DEDENT')
|
|
308
|
+
node = Unless(condition, body, else_body)
|
|
309
|
+
node.line = token.line
|
|
310
|
+
return node
|
|
311
|
+
def parse_until(self) -> Until:
|
|
312
|
+
token = self.consume('UNTIL')
|
|
313
|
+
condition = self.parse_expression()
|
|
314
|
+
self.consume('NEWLINE')
|
|
315
|
+
self.consume('INDENT')
|
|
316
|
+
body = []
|
|
317
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
318
|
+
while self.check('NEWLINE'): self.consume()
|
|
319
|
+
if self.check('DEDENT'): break
|
|
320
|
+
body.append(self.parse_statement())
|
|
321
|
+
self.consume('DEDENT')
|
|
322
|
+
node = Until(condition, body)
|
|
323
|
+
node.line = token.line
|
|
324
|
+
return node
|
|
325
|
+
def parse_forever(self) -> Forever:
|
|
326
|
+
token = self.consume('FOREVER')
|
|
327
|
+
self.consume('NEWLINE')
|
|
328
|
+
self.consume('INDENT')
|
|
329
|
+
body = []
|
|
330
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
331
|
+
while self.check('NEWLINE'): self.consume()
|
|
332
|
+
if self.check('DEDENT'): break
|
|
333
|
+
body.append(self.parse_statement())
|
|
334
|
+
self.consume('DEDENT')
|
|
335
|
+
node = Forever(body)
|
|
336
|
+
node.line = token.line
|
|
337
|
+
return node
|
|
338
|
+
def parse_exit(self) -> Exit:
|
|
339
|
+
token = self.consume('EXIT')
|
|
340
|
+
code = None
|
|
341
|
+
if not self.check('NEWLINE'):
|
|
342
|
+
code = self.parse_expression()
|
|
343
|
+
self.consume('NEWLINE')
|
|
344
|
+
node = Exit(code)
|
|
345
|
+
node.line = token.line
|
|
346
|
+
return node
|
|
347
|
+
def parse_make(self) -> Node:
|
|
348
|
+
node = self.parse_make_expr()
|
|
349
|
+
self.consume('NEWLINE')
|
|
350
|
+
return node
|
|
351
|
+
|
|
352
|
+
def parse_make_expr(self) -> Node:
|
|
353
|
+
token = self.consume('MAKE')
|
|
354
|
+
class_name = self.consume('ID').value
|
|
355
|
+
|
|
356
|
+
if self.check('BE'):
|
|
357
|
+
self.consume('BE')
|
|
358
|
+
value = self.parse_expression()
|
|
359
|
+
node = Assign(class_name, value) # class_name is actually variable name here
|
|
360
|
+
node.line = token.line
|
|
361
|
+
return node
|
|
362
|
+
|
|
363
|
+
args = []
|
|
364
|
+
if self.check('LPAREN'):
|
|
365
|
+
self.consume('LPAREN')
|
|
366
|
+
if not self.check('RPAREN'):
|
|
367
|
+
args.append(self.parse_expression())
|
|
368
|
+
while self.check('COMMA'):
|
|
369
|
+
self.consume('COMMA')
|
|
370
|
+
args.append(self.parse_expression())
|
|
371
|
+
self.consume('RPAREN')
|
|
372
|
+
else:
|
|
373
|
+
while not self.check('NEWLINE') and not self.check('EOF'):
|
|
374
|
+
args.append(self.parse_expression())
|
|
375
|
+
node = Make(class_name, args)
|
|
376
|
+
node.line = token.line
|
|
377
|
+
return node
|
|
378
|
+
return node
|
|
379
|
+
def parse_db_op(self) -> DatabaseOp:
|
|
380
|
+
token = self.consume('DB')
|
|
381
|
+
op = 'open'
|
|
382
|
+
if self.check('OPEN'): op = 'open'; self.consume()
|
|
383
|
+
elif self.check('QUERY'): op = 'query'; self.consume()
|
|
384
|
+
elif self.check('EXEC'): op = 'exec'; self.consume()
|
|
385
|
+
elif self.check('CLOSE'): op = 'close'; self.consume()
|
|
386
|
+
else:
|
|
387
|
+
if self.check('STRING'):
|
|
388
|
+
op = 'open'
|
|
389
|
+
else:
|
|
390
|
+
raise SyntaxError(f"Unknown db operation at line {token.line}")
|
|
391
|
+
args = []
|
|
392
|
+
if op != 'close' and not self.check('NEWLINE'):
|
|
393
|
+
args.append(self.parse_expression())
|
|
394
|
+
while not self.check('NEWLINE'):
|
|
395
|
+
args.append(self.parse_expression())
|
|
396
|
+
node = DatabaseOp(op, args)
|
|
397
|
+
node.line = token.line
|
|
398
|
+
return node
|
|
399
|
+
def parse_middleware(self) -> Node:
|
|
400
|
+
token = self.consume('BEFORE')
|
|
401
|
+
self.consume('REQUEST')
|
|
402
|
+
self.consume('NEWLINE')
|
|
403
|
+
self.consume('INDENT')
|
|
404
|
+
body = []
|
|
405
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
406
|
+
while self.check('NEWLINE'): self.consume()
|
|
407
|
+
if self.check('DEDENT'): break
|
|
408
|
+
body.append(self.parse_statement())
|
|
409
|
+
self.consume('DEDENT')
|
|
410
|
+
return OnRequest(String('__middleware__'), body)
|
|
411
|
+
def parse_when(self) -> Node:
|
|
412
|
+
token = self.consume('WHEN')
|
|
413
|
+
if self.check('SOMEONE'):
|
|
414
|
+
self.consume('SOMEONE')
|
|
415
|
+
if self.check('VISITS'):
|
|
416
|
+
self.consume('VISITS')
|
|
417
|
+
path = String(self.consume('STRING').value)
|
|
418
|
+
self.consume('NEWLINE')
|
|
419
|
+
self.consume('INDENT')
|
|
420
|
+
body = []
|
|
421
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
422
|
+
while self.check('NEWLINE'): self.consume()
|
|
423
|
+
if self.check('DEDENT'): break
|
|
424
|
+
body.append(self.parse_statement())
|
|
425
|
+
self.consume('DEDENT')
|
|
426
|
+
node = OnRequest(path, body)
|
|
427
|
+
node.line = token.line
|
|
428
|
+
return node
|
|
429
|
+
elif self.check('SUBMITS'):
|
|
430
|
+
self.consume('SUBMITS')
|
|
431
|
+
if self.check('TO'):
|
|
432
|
+
self.consume('TO')
|
|
433
|
+
path = String(self.consume('STRING').value)
|
|
434
|
+
self.consume('NEWLINE')
|
|
435
|
+
self.consume('INDENT')
|
|
436
|
+
body = []
|
|
437
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
438
|
+
while self.check('NEWLINE'): self.consume()
|
|
439
|
+
if self.check('DEDENT'): break
|
|
440
|
+
body.append(self.parse_statement())
|
|
441
|
+
self.consume('DEDENT')
|
|
442
|
+
node = OnRequest(path, body)
|
|
443
|
+
node.line = token.line
|
|
444
|
+
return node
|
|
445
|
+
condition_or_value = self.parse_expression()
|
|
446
|
+
self.consume('NEWLINE')
|
|
447
|
+
self.consume('INDENT')
|
|
448
|
+
if self.check('IS'):
|
|
449
|
+
cases = []
|
|
450
|
+
otherwise = None
|
|
451
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
452
|
+
if self.check('IS'):
|
|
453
|
+
self.consume('IS')
|
|
454
|
+
match_val = self.parse_expression()
|
|
455
|
+
self.consume('NEWLINE')
|
|
456
|
+
self.consume('INDENT')
|
|
457
|
+
case_body = []
|
|
458
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
459
|
+
while self.check('NEWLINE'): self.consume()
|
|
460
|
+
if self.check('DEDENT'): break
|
|
461
|
+
case_body.append(self.parse_statement())
|
|
462
|
+
self.consume('DEDENT')
|
|
463
|
+
cases.append((match_val, case_body))
|
|
464
|
+
elif self.check('OTHERWISE'):
|
|
465
|
+
self.consume('OTHERWISE')
|
|
466
|
+
self.consume('NEWLINE')
|
|
467
|
+
self.consume('INDENT')
|
|
468
|
+
otherwise = []
|
|
469
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
470
|
+
while self.check('NEWLINE'): self.consume()
|
|
471
|
+
if self.check('DEDENT'): break
|
|
472
|
+
otherwise.append(self.parse_statement())
|
|
473
|
+
self.consume('DEDENT')
|
|
474
|
+
elif self.check('NEWLINE'):
|
|
475
|
+
self.consume('NEWLINE')
|
|
476
|
+
else:
|
|
477
|
+
break
|
|
478
|
+
self.consume('DEDENT')
|
|
479
|
+
node = When(condition_or_value, cases, otherwise)
|
|
480
|
+
node.line = token.line
|
|
481
|
+
return node
|
|
482
|
+
else:
|
|
483
|
+
body = []
|
|
484
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
485
|
+
while self.check('NEWLINE'): self.consume()
|
|
486
|
+
if self.check('DEDENT'): break
|
|
487
|
+
body.append(self.parse_statement())
|
|
488
|
+
self.consume('DEDENT')
|
|
489
|
+
else_body = None
|
|
490
|
+
if self.check('ELSE'):
|
|
491
|
+
self.consume('ELSE')
|
|
492
|
+
self.consume('NEWLINE')
|
|
493
|
+
self.consume('INDENT')
|
|
494
|
+
else_body = []
|
|
495
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
496
|
+
while self.check('NEWLINE'): self.consume()
|
|
497
|
+
if self.check('DEDENT'): break
|
|
498
|
+
else_body.append(self.parse_statement())
|
|
499
|
+
self.consume('DEDENT')
|
|
500
|
+
node = If(condition_or_value, body, else_body)
|
|
501
|
+
node.line = token.line
|
|
502
|
+
return node
|
|
503
|
+
def parse_return(self) -> Return:
|
|
504
|
+
token = self.consume('RETURN')
|
|
505
|
+
expr = self.parse_expression()
|
|
506
|
+
self.consume('NEWLINE')
|
|
507
|
+
node = Return(expr)
|
|
508
|
+
node.line = token.line
|
|
509
|
+
return node
|
|
510
|
+
def parse_function_def(self) -> FunctionDef:
|
|
511
|
+
start_token = None
|
|
512
|
+
if self.check('DEFINE'):
|
|
513
|
+
start_token = self.consume('DEFINE')
|
|
514
|
+
self.consume('FUNCTION')
|
|
515
|
+
else:
|
|
516
|
+
start_token = self.consume('TO') # Fallback to existing 'TO' if not 'DEFINE'
|
|
517
|
+
|
|
518
|
+
name = self.consume('ID').value
|
|
519
|
+
args = []
|
|
520
|
+
while self.check('ID'):
|
|
521
|
+
arg_name = self.consume('ID').value
|
|
522
|
+
type_hint = None
|
|
523
|
+
if self.check('COLON'):
|
|
524
|
+
if self.peek(1).type == 'NEWLINE':
|
|
525
|
+
pass
|
|
526
|
+
else:
|
|
527
|
+
self.consume('COLON')
|
|
528
|
+
if self.check('ID'):
|
|
529
|
+
type_hint = self.consume('ID').value
|
|
530
|
+
elif self.check('STRING'):
|
|
531
|
+
type_hint = "str"
|
|
532
|
+
self.consume()
|
|
533
|
+
else:
|
|
534
|
+
type_hint = self.consume().value
|
|
535
|
+
default_val = None
|
|
536
|
+
if self.check('ASSIGN'):
|
|
537
|
+
self.consume('ASSIGN')
|
|
538
|
+
default_val = self.parse_expression()
|
|
539
|
+
args.append((arg_name, default_val, type_hint))
|
|
540
|
+
if self.check('DOING'): self.consume('DOING')
|
|
541
|
+
if self.check('COLON'):
|
|
542
|
+
self.consume('COLON')
|
|
543
|
+
self.consume('NEWLINE')
|
|
544
|
+
self.consume('INDENT')
|
|
545
|
+
body = []
|
|
546
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
547
|
+
while self.check('NEWLINE'): self.consume()
|
|
548
|
+
if self.check('DEDENT'): break
|
|
549
|
+
body.append(self.parse_statement())
|
|
550
|
+
self.consume('DEDENT')
|
|
551
|
+
node = FunctionDef(name, args, body)
|
|
552
|
+
node.line = start_token.line
|
|
553
|
+
return node
|
|
554
|
+
def parse_class_def(self) -> ClassDef:
|
|
555
|
+
start_token = self.consume('STRUCTURE')
|
|
556
|
+
name = self.consume('ID').value
|
|
557
|
+
parent = None
|
|
558
|
+
if self.check('EXTENDS'):
|
|
559
|
+
self.consume('EXTENDS')
|
|
560
|
+
parent = self.consume('ID').value
|
|
561
|
+
elif self.check('LPAREN'):
|
|
562
|
+
self.consume('LPAREN')
|
|
563
|
+
parent = self.consume('ID').value
|
|
564
|
+
self.consume('RPAREN')
|
|
565
|
+
self.consume('NEWLINE')
|
|
566
|
+
self.consume('INDENT')
|
|
567
|
+
properties = []
|
|
568
|
+
methods = []
|
|
569
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
570
|
+
if self.check('HAS'):
|
|
571
|
+
self.consume()
|
|
572
|
+
prop_name = None
|
|
573
|
+
if self.check('MAKE'):
|
|
574
|
+
prop_name = self.consume().value
|
|
575
|
+
else:
|
|
576
|
+
prop_name = self.consume('ID').value
|
|
577
|
+
|
|
578
|
+
default_val = None
|
|
579
|
+
if self.check('ASSIGN'):
|
|
580
|
+
self.consume('ASSIGN')
|
|
581
|
+
default_val = self.parse_expression()
|
|
582
|
+
|
|
583
|
+
properties.append((prop_name, default_val))
|
|
584
|
+
self.consume('NEWLINE')
|
|
585
|
+
elif self.check('TO'):
|
|
586
|
+
methods.append(self.parse_function_def())
|
|
587
|
+
elif self.check('ID'):
|
|
588
|
+
prop_name = self.consume('ID').value
|
|
589
|
+
default_val = None
|
|
590
|
+
if self.check('ASSIGN'):
|
|
591
|
+
self.consume('ASSIGN')
|
|
592
|
+
default_val = self.parse_expression()
|
|
593
|
+
properties.append((prop_name, default_val))
|
|
594
|
+
self.consume('NEWLINE')
|
|
595
|
+
elif self.check('NEWLINE'):
|
|
596
|
+
self.consume()
|
|
597
|
+
else:
|
|
598
|
+
self.consume('DEDENT')
|
|
599
|
+
break
|
|
600
|
+
self.consume('DEDENT')
|
|
601
|
+
node = ClassDef(name, properties, methods, parent)
|
|
602
|
+
node.line = start_token.line
|
|
603
|
+
return node
|
|
604
|
+
def parse_id_start_statement(self, passed_name_token=None) -> Node:
|
|
605
|
+
if passed_name_token:
|
|
606
|
+
name_token = passed_name_token
|
|
607
|
+
else:
|
|
608
|
+
name_token = self.consume('ID')
|
|
609
|
+
name = name_token.value
|
|
610
|
+
if self.check('ASSIGN'):
|
|
611
|
+
self.consume('ASSIGN')
|
|
612
|
+
value = self.parse_expression()
|
|
613
|
+
self.consume('NEWLINE')
|
|
614
|
+
node = Assign(name, value)
|
|
615
|
+
node.line = name_token.line
|
|
616
|
+
return node
|
|
617
|
+
elif self.check('PLUSEQ'):
|
|
618
|
+
self.consume('PLUSEQ')
|
|
619
|
+
value = self.parse_expression()
|
|
620
|
+
self.consume('NEWLINE')
|
|
621
|
+
node = Assign(name, BinOp(VarAccess(name), '+', value))
|
|
622
|
+
node.line = name_token.line
|
|
623
|
+
return node
|
|
624
|
+
elif self.check('MINUSEQ'):
|
|
625
|
+
self.consume('MINUSEQ')
|
|
626
|
+
value = self.parse_expression()
|
|
627
|
+
self.consume('NEWLINE')
|
|
628
|
+
node = Assign(name, BinOp(VarAccess(name), '-', value))
|
|
629
|
+
node.line = name_token.line
|
|
630
|
+
return node
|
|
631
|
+
elif self.check('MULEQ'):
|
|
632
|
+
self.consume('MULEQ')
|
|
633
|
+
value = self.parse_expression()
|
|
634
|
+
self.consume('NEWLINE')
|
|
635
|
+
node = Assign(name, BinOp(VarAccess(name), '*', value))
|
|
636
|
+
node.line = name_token.line
|
|
637
|
+
return node
|
|
638
|
+
elif self.check('DIVEQ'):
|
|
639
|
+
self.consume('DIVEQ')
|
|
640
|
+
value = self.parse_expression()
|
|
641
|
+
self.consume('NEWLINE')
|
|
642
|
+
node = Assign(name, BinOp(VarAccess(name), '/', value))
|
|
643
|
+
node.line = name_token.line
|
|
644
|
+
return node
|
|
645
|
+
elif self.check('IS'):
|
|
646
|
+
token_is = self.consume('IS')
|
|
647
|
+
if self.check('ID') and self.peek().value == 'a':
|
|
648
|
+
self.consume()
|
|
649
|
+
if self.check('LIST'):
|
|
650
|
+
self.consume('LIST')
|
|
651
|
+
self.consume('NEWLINE')
|
|
652
|
+
node = Assign(name, ListVal([]))
|
|
653
|
+
node.line = token_is.line
|
|
654
|
+
return node
|
|
655
|
+
if self.check('ID') and self.peek().value in ('dictionary', 'map', 'dict'):
|
|
656
|
+
self.consume()
|
|
657
|
+
self.consume('NEWLINE')
|
|
658
|
+
node = Assign(name, Dictionary([]))
|
|
659
|
+
node.line = token_is.line
|
|
660
|
+
return node
|
|
661
|
+
if self.check('ID') and not self.peek().value in ('{', '['):
|
|
662
|
+
class_name = self.consume('ID').value
|
|
663
|
+
args = []
|
|
664
|
+
while not self.check('NEWLINE') and not self.check('EOF'):
|
|
665
|
+
args.append(self.parse_expression())
|
|
666
|
+
self.consume('NEWLINE')
|
|
667
|
+
node = Instantiation(name, class_name, args)
|
|
668
|
+
node.line = token_is.line
|
|
669
|
+
return node
|
|
670
|
+
else:
|
|
671
|
+
value = self.parse_expression()
|
|
672
|
+
self.consume('NEWLINE')
|
|
673
|
+
node = Assign(name, value)
|
|
674
|
+
node.line = token_is.line
|
|
675
|
+
return node
|
|
676
|
+
elif self.check('LBRACKET'):
|
|
677
|
+
self.consume('LBRACKET')
|
|
678
|
+
index = self.parse_expression()
|
|
679
|
+
self.consume('RBRACKET')
|
|
680
|
+
self.consume('ASSIGN')
|
|
681
|
+
val = self.parse_expression()
|
|
682
|
+
self.consume('NEWLINE')
|
|
683
|
+
# Convert simple assignment to PropertyAssign-like or specific set call?
|
|
684
|
+
# interpreter.visit_IndexAccess handles get.
|
|
685
|
+
# We need a node for IndexAssign. parser snippet doesn't show one.
|
|
686
|
+
# But visit_PropertyAssign handles dict keys.
|
|
687
|
+
# Let's map d[k] = v to PropertyAssign(d, k, v)?
|
|
688
|
+
# No, PropertyAssign takes member name as string. Here index handles expressions.
|
|
689
|
+
# I need to check if there is a node for this.
|
|
690
|
+
# If not, I'll return Call('set', [VarAccess(name), index, val])
|
|
691
|
+
return Call('set', [VarAccess(name), index, val])
|
|
692
|
+
elif self.check('DOT'):
|
|
693
|
+
self.consume('DOT')
|
|
694
|
+
member_token = self.consume()
|
|
695
|
+
member = member_token.value
|
|
696
|
+
if self.check('ASSIGN'):
|
|
697
|
+
self.consume('ASSIGN')
|
|
698
|
+
value = self.parse_expression()
|
|
699
|
+
self.consume('NEWLINE')
|
|
700
|
+
return PropertyAssign(name, member, value)
|
|
701
|
+
|
|
702
|
+
args = []
|
|
703
|
+
if self.check('LPAREN'):
|
|
704
|
+
self.consume('LPAREN')
|
|
705
|
+
if not self.check('RPAREN'):
|
|
706
|
+
args.append(self.parse_expression())
|
|
707
|
+
while self.check('COMMA'):
|
|
708
|
+
self.consume('COMMA')
|
|
709
|
+
args.append(self.parse_expression())
|
|
710
|
+
self.consume('RPAREN')
|
|
711
|
+
self.consume('NEWLINE')
|
|
712
|
+
node = MethodCall(name, member, args)
|
|
713
|
+
node.line = name_token.line
|
|
714
|
+
return node
|
|
715
|
+
|
|
716
|
+
while not self.check('NEWLINE') and not self.check('EOF'):
|
|
717
|
+
args.append(self.parse_expression())
|
|
718
|
+
self.consume('NEWLINE')
|
|
719
|
+
node = MethodCall(name, member, args)
|
|
720
|
+
node.line = name_token.line
|
|
721
|
+
return node
|
|
722
|
+
elif self.check('LPAREN'):
|
|
723
|
+
self.consume('LPAREN')
|
|
724
|
+
args = []
|
|
725
|
+
if not self.check('RPAREN'):
|
|
726
|
+
args.append(self.parse_expression())
|
|
727
|
+
while self.check('COMMA'):
|
|
728
|
+
self.consume('COMMA')
|
|
729
|
+
args.append(self.parse_expression())
|
|
730
|
+
self.consume('RPAREN')
|
|
731
|
+
self.consume('NEWLINE')
|
|
732
|
+
node = Call(name, args)
|
|
733
|
+
node.line = name_token.line
|
|
734
|
+
return node
|
|
735
|
+
else:
|
|
736
|
+
if not self.check('NEWLINE') and not self.check('EOF') and not self.check('EQ') and not self.check('IS'):
|
|
737
|
+
args = []
|
|
738
|
+
while not self.check('NEWLINE') and not self.check('EOF') and not self.check('IS'):
|
|
739
|
+
is_named_arg = False
|
|
740
|
+
if self.peek(1).type == 'ASSIGN':
|
|
741
|
+
t_type = self.peek().type
|
|
742
|
+
if t_type in ('ID', 'STRUCTURE', 'TYPE', 'FOR', 'IN', 'WHILE', 'IF', 'ELSE', 'FROM', 'TO', 'STRING', 'EXTENDS', 'WITH', 'PLACEHOLDER', 'NAME', 'VALUE', 'ACTION', 'METHOD', 'HREF', 'SRC', 'CLASS', 'STYLE'):
|
|
743
|
+
is_named_arg = True
|
|
744
|
+
if is_named_arg:
|
|
745
|
+
key_token = self.consume()
|
|
746
|
+
key = key_token.value
|
|
747
|
+
self.consume('ASSIGN')
|
|
748
|
+
val = self.parse_expression()
|
|
749
|
+
args.append(Dictionary([ (String(key), val) ]))
|
|
750
|
+
else:
|
|
751
|
+
if self.check('USING'):
|
|
752
|
+
self.consume('USING')
|
|
753
|
+
args.append(self.parse_expression())
|
|
754
|
+
if self.check('NEWLINE'):
|
|
755
|
+
self.consume('NEWLINE')
|
|
756
|
+
elif self.check('INDENT'):
|
|
757
|
+
pass
|
|
758
|
+
else:
|
|
759
|
+
self.consume('NEWLINE')
|
|
760
|
+
body = None
|
|
761
|
+
if self.check('INDENT'):
|
|
762
|
+
self.consume('INDENT')
|
|
763
|
+
body = []
|
|
764
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
765
|
+
while self.check('NEWLINE'): self.consume()
|
|
766
|
+
if self.check('DEDENT'): break
|
|
767
|
+
body.append(self.parse_statement())
|
|
768
|
+
self.consume('DEDENT')
|
|
769
|
+
node = Call(name, args, body)
|
|
770
|
+
node.line = name_token.line
|
|
771
|
+
return node
|
|
772
|
+
node = Call(name, args, body)
|
|
773
|
+
node.line = name_token.line
|
|
774
|
+
return node
|
|
775
|
+
if self.check('NEWLINE'):
|
|
776
|
+
self.consume('NEWLINE')
|
|
777
|
+
elif self.check('INDENT'):
|
|
778
|
+
pass
|
|
779
|
+
else:
|
|
780
|
+
self.consume('NEWLINE')
|
|
781
|
+
if self.check('INDENT'):
|
|
782
|
+
self.consume('INDENT')
|
|
783
|
+
body = []
|
|
784
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
785
|
+
while self.check('NEWLINE'): self.consume()
|
|
786
|
+
if self.check('DEDENT'): break
|
|
787
|
+
body.append(self.parse_statement())
|
|
788
|
+
self.consume('DEDENT')
|
|
789
|
+
node = Call(name, [], body)
|
|
790
|
+
node.line = name_token.line
|
|
791
|
+
return node
|
|
792
|
+
node = VarAccess(name)
|
|
793
|
+
node.line = name_token.line
|
|
794
|
+
return node
|
|
795
|
+
def parse_print(self) -> Node:
|
|
796
|
+
if self.check('PRINT'):
|
|
797
|
+
token = self.consume('PRINT')
|
|
798
|
+
else:
|
|
799
|
+
token = self.consume('SAY')
|
|
800
|
+
if self.check('PROGRESS'):
|
|
801
|
+
return self.parse_progress_loop(token)
|
|
802
|
+
style = None
|
|
803
|
+
color = None
|
|
804
|
+
if self.check('IN'):
|
|
805
|
+
self.consume('IN')
|
|
806
|
+
if self.peek().type in ('RED', 'GREEN', 'BLUE', 'YELLOW', 'CYAN', 'MAGENTA'):
|
|
807
|
+
color = self.consume().value
|
|
808
|
+
if self.check('BOLD'):
|
|
809
|
+
self.consume('BOLD')
|
|
810
|
+
style = 'bold'
|
|
811
|
+
if self.peek().type in ('RED', 'GREEN', 'BLUE', 'YELLOW', 'CYAN', 'MAGENTA'):
|
|
812
|
+
color = self.consume().value
|
|
813
|
+
expr = self.parse_expression()
|
|
814
|
+
self.consume('NEWLINE')
|
|
815
|
+
node = Print(expression=expr, style=style, color=color)
|
|
816
|
+
node.line = token.line
|
|
817
|
+
return node
|
|
818
|
+
def parse_progress_loop(self, start_token: Token) -> ProgressLoop:
|
|
819
|
+
self.consume('PROGRESS')
|
|
820
|
+
if not (self.check('FOR') or self.check('REPEAT') or self.check('LOOP')):
|
|
821
|
+
raise SyntaxError(f"Expected loop after 'show progress' on line {start_token.line}")
|
|
822
|
+
if self.check('FOR') or self.check('LOOP'):
|
|
823
|
+
loop_node = self.parse_for()
|
|
824
|
+
else:
|
|
825
|
+
loop_node = self.parse_repeat()
|
|
826
|
+
node = ProgressLoop(loop_node)
|
|
827
|
+
node.line = start_token.line
|
|
828
|
+
return node
|
|
829
|
+
def parse_serve(self) -> ServeStatic:
|
|
830
|
+
token = self.consume('SERVE')
|
|
831
|
+
if self.check('FILES'):
|
|
832
|
+
self.consume('FILES')
|
|
833
|
+
if self.check('FROM'):
|
|
834
|
+
self.consume('FROM')
|
|
835
|
+
folder = self.parse_expression()
|
|
836
|
+
url = String('/static')
|
|
837
|
+
if self.check('AT'):
|
|
838
|
+
self.consume('AT')
|
|
839
|
+
url = self.parse_expression()
|
|
840
|
+
if self.check('FOLDER'):
|
|
841
|
+
self.consume('FOLDER')
|
|
842
|
+
self.consume('NEWLINE')
|
|
843
|
+
node = ServeStatic(folder, url)
|
|
844
|
+
node.line = token.line
|
|
845
|
+
return node
|
|
846
|
+
self.consume('STATIC')
|
|
847
|
+
folder = self.parse_expression()
|
|
848
|
+
self.consume('AT')
|
|
849
|
+
url = self.parse_expression()
|
|
850
|
+
self.consume('NEWLINE')
|
|
851
|
+
node = ServeStatic(folder, url)
|
|
852
|
+
node.line = token.line
|
|
853
|
+
return node
|
|
854
|
+
def parse_listen(self) -> Listen:
|
|
855
|
+
token = self.consume('LISTEN')
|
|
856
|
+
if self.check('ON'): self.consume('ON')
|
|
857
|
+
if self.check('PORT'): self.consume('PORT')
|
|
858
|
+
port_num = self.parse_expression()
|
|
859
|
+
self.consume('NEWLINE')
|
|
860
|
+
node = Listen(port_num)
|
|
861
|
+
node.line = token.line
|
|
862
|
+
return node
|
|
863
|
+
def parse_every(self) -> Every:
|
|
864
|
+
token = self.consume('EVERY')
|
|
865
|
+
interval = self.parse_expression()
|
|
866
|
+
unit = 'seconds'
|
|
867
|
+
if self.check('MINUTE'):
|
|
868
|
+
self.consume()
|
|
869
|
+
unit = 'minutes'
|
|
870
|
+
elif self.check('SECOND'):
|
|
871
|
+
self.consume()
|
|
872
|
+
unit = 'seconds'
|
|
873
|
+
self.consume('NEWLINE')
|
|
874
|
+
self.consume('INDENT')
|
|
875
|
+
body = []
|
|
876
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
877
|
+
while self.check('NEWLINE'): self.consume()
|
|
878
|
+
if self.check('DEDENT'): break
|
|
879
|
+
body.append(self.parse_statement())
|
|
880
|
+
self.consume('DEDENT')
|
|
881
|
+
node = Every(interval, unit, body)
|
|
882
|
+
node.line = token.line
|
|
883
|
+
return node
|
|
884
|
+
def parse_after(self) -> After:
|
|
885
|
+
token = self.consume('IN')
|
|
886
|
+
delay = self.parse_expression()
|
|
887
|
+
unit = 'seconds'
|
|
888
|
+
if self.check('MINUTE'):
|
|
889
|
+
self.consume()
|
|
890
|
+
unit = 'minutes'
|
|
891
|
+
elif self.check('SECOND'):
|
|
892
|
+
self.consume()
|
|
893
|
+
unit = 'seconds'
|
|
894
|
+
self.consume('NEWLINE')
|
|
895
|
+
self.consume('INDENT')
|
|
896
|
+
body = []
|
|
897
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
898
|
+
while self.check('NEWLINE'): self.consume()
|
|
899
|
+
if self.check('DEDENT'): break
|
|
900
|
+
body.append(self.parse_statement())
|
|
901
|
+
self.consume('DEDENT')
|
|
902
|
+
node = After(delay, unit, body)
|
|
903
|
+
node.line = token.line
|
|
904
|
+
return node
|
|
905
|
+
def parse_define_page(self) -> Node:
|
|
906
|
+
token = self.consume('DEFINE')
|
|
907
|
+
if self.check('PAGE'):
|
|
908
|
+
self.consume('PAGE')
|
|
909
|
+
name = self.consume('ID').value
|
|
910
|
+
args = []
|
|
911
|
+
if self.check('USING'):
|
|
912
|
+
self.consume('USING')
|
|
913
|
+
args.append((self.consume('ID').value, None, None))
|
|
914
|
+
while self.check('COMMA'):
|
|
915
|
+
self.consume('COMMA')
|
|
916
|
+
args.append((self.consume('ID').value, None, None))
|
|
917
|
+
self.consume('NEWLINE')
|
|
918
|
+
self.consume('INDENT')
|
|
919
|
+
body = []
|
|
920
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
921
|
+
while self.check('NEWLINE'): self.consume()
|
|
922
|
+
if self.check('DEDENT'): break
|
|
923
|
+
body.append(self.parse_statement())
|
|
924
|
+
self.consume('DEDENT')
|
|
925
|
+
node = FunctionDef(name, args, body)
|
|
926
|
+
node.line = token.line
|
|
927
|
+
return node
|
|
928
|
+
def parse_add_to(self) -> Node:
|
|
929
|
+
token = self.consume('ADD')
|
|
930
|
+
item_expr = self.parse_factor_simple()
|
|
931
|
+
if self.check('TO') or self.check('INTO'):
|
|
932
|
+
self.consume()
|
|
933
|
+
list_name = self.consume('ID').value
|
|
934
|
+
self.consume('NEWLINE')
|
|
935
|
+
list_access = VarAccess(list_name)
|
|
936
|
+
item_list = ListVal([item_expr])
|
|
937
|
+
concat = BinOp(list_access, '+', item_list)
|
|
938
|
+
node = Assign(list_name, concat)
|
|
939
|
+
node.line = token.line
|
|
940
|
+
return node
|
|
941
|
+
def parse_start_server(self) -> Node:
|
|
942
|
+
token = self.consume('START')
|
|
943
|
+
if self.check('SERVER'):
|
|
944
|
+
self.consume('SERVER')
|
|
945
|
+
port = Number(8080)
|
|
946
|
+
if self.check('ON'):
|
|
947
|
+
self.consume('ON')
|
|
948
|
+
if self.check('PORT'):
|
|
949
|
+
self.consume('PORT')
|
|
950
|
+
port = self.parse_expression()
|
|
951
|
+
self.consume('NEWLINE')
|
|
952
|
+
node = Listen(port)
|
|
953
|
+
node.line = token.line
|
|
954
|
+
return node
|
|
955
|
+
def parse_heading(self) -> Node:
|
|
956
|
+
token = self.consume('HEADING')
|
|
957
|
+
text = self.parse_expression()
|
|
958
|
+
self.consume('NEWLINE')
|
|
959
|
+
node = Call('h1', [text])
|
|
960
|
+
node.line = token.line
|
|
961
|
+
return node
|
|
962
|
+
def parse_paragraph(self) -> Node:
|
|
963
|
+
token = self.consume('PARAGRAPH')
|
|
964
|
+
text = self.parse_expression()
|
|
965
|
+
self.consume('NEWLINE')
|
|
966
|
+
node = Call('p', [text])
|
|
967
|
+
node.line = token.line
|
|
968
|
+
return node
|
|
969
|
+
def parse_assign(self) -> Assign:
|
|
970
|
+
name = self.consume('ID').value
|
|
971
|
+
self.consume('ASSIGN')
|
|
972
|
+
value = self.parse_expression()
|
|
973
|
+
self.consume('NEWLINE')
|
|
974
|
+
return Assign(name, value)
|
|
975
|
+
def parse_import(self) -> Node:
|
|
976
|
+
token = self.consume('USE')
|
|
977
|
+
|
|
978
|
+
# Check for Python Import: use python "numpy" as np
|
|
979
|
+
if self.check('ID') and self.peek().value == 'python':
|
|
980
|
+
self.consume('ID') # consume 'python'
|
|
981
|
+
if self.check('STRING'):
|
|
982
|
+
module_name = self.consume('STRING').value
|
|
983
|
+
alias = None
|
|
984
|
+
if self.check('AS'):
|
|
985
|
+
self.consume('AS')
|
|
986
|
+
alias = self.consume('ID').value
|
|
987
|
+
self.consume('NEWLINE')
|
|
988
|
+
node = PythonImport(module_name, alias)
|
|
989
|
+
node.line = token.line
|
|
990
|
+
return node
|
|
991
|
+
else:
|
|
992
|
+
raise SyntaxError(f"Expected module name string after 'use python' at line {token.line}")
|
|
993
|
+
|
|
994
|
+
if self.check('STRING'):
|
|
995
|
+
path = self.consume('STRING').value
|
|
996
|
+
else:
|
|
997
|
+
path = self.consume('ID').value
|
|
998
|
+
if self.check('AS'):
|
|
999
|
+
self.consume('AS')
|
|
1000
|
+
alias = self.consume('ID').value
|
|
1001
|
+
self.consume('NEWLINE')
|
|
1002
|
+
node = ImportAs(path, alias)
|
|
1003
|
+
else:
|
|
1004
|
+
self.consume('NEWLINE')
|
|
1005
|
+
node = Import(path)
|
|
1006
|
+
node.line = token.line
|
|
1007
|
+
return node
|
|
1008
|
+
def parse_if(self) -> If:
|
|
1009
|
+
self.consume('IF')
|
|
1010
|
+
condition = self.parse_expression()
|
|
1011
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1012
|
+
self.consume('NEWLINE')
|
|
1013
|
+
self.consume('INDENT')
|
|
1014
|
+
body = []
|
|
1015
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1016
|
+
while self.check('NEWLINE'): self.consume()
|
|
1017
|
+
if self.check('DEDENT'): break
|
|
1018
|
+
body.append(self.parse_statement())
|
|
1019
|
+
self.consume('DEDENT')
|
|
1020
|
+
else_body = None
|
|
1021
|
+
if self.check('ELIF'):
|
|
1022
|
+
else_body = [self.parse_elif()]
|
|
1023
|
+
elif self.check('ELSE') or self.check('OTHERWISE'):
|
|
1024
|
+
if self.check('ELSE'): self.consume('ELSE')
|
|
1025
|
+
else: self.consume('OTHERWISE')
|
|
1026
|
+
|
|
1027
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1028
|
+
self.consume('NEWLINE')
|
|
1029
|
+
self.consume('INDENT')
|
|
1030
|
+
else_body = []
|
|
1031
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1032
|
+
while self.check('NEWLINE'): self.consume()
|
|
1033
|
+
if self.check('DEDENT'): break
|
|
1034
|
+
else_body.append(self.parse_statement())
|
|
1035
|
+
self.consume('DEDENT')
|
|
1036
|
+
return If(condition, body, else_body)
|
|
1037
|
+
|
|
1038
|
+
def parse_elif(self) -> If:
|
|
1039
|
+
token = self.consume('ELIF')
|
|
1040
|
+
condition = self.parse_expression()
|
|
1041
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1042
|
+
self.consume('NEWLINE')
|
|
1043
|
+
self.consume('INDENT')
|
|
1044
|
+
body = []
|
|
1045
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1046
|
+
while self.check('NEWLINE'): self.consume()
|
|
1047
|
+
if self.check('DEDENT'): break
|
|
1048
|
+
body.append(self.parse_statement())
|
|
1049
|
+
self.consume('DEDENT')
|
|
1050
|
+
else_body = None
|
|
1051
|
+
if self.check('ELIF'):
|
|
1052
|
+
else_body = [self.parse_elif()]
|
|
1053
|
+
elif self.check('ELSE'):
|
|
1054
|
+
self.consume('ELSE')
|
|
1055
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1056
|
+
self.consume('NEWLINE')
|
|
1057
|
+
self.consume('INDENT')
|
|
1058
|
+
else_body = []
|
|
1059
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1060
|
+
while self.check('NEWLINE'): self.consume()
|
|
1061
|
+
if self.check('DEDENT'): break
|
|
1062
|
+
else_body.append(self.parse_statement())
|
|
1063
|
+
self.consume('DEDENT')
|
|
1064
|
+
return If(condition, body, else_body)
|
|
1065
|
+
|
|
1066
|
+
def parse_while(self) -> While:
|
|
1067
|
+
start_token = self.consume('WHILE')
|
|
1068
|
+
condition = self.parse_expression()
|
|
1069
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1070
|
+
self.consume('NEWLINE')
|
|
1071
|
+
self.consume('INDENT')
|
|
1072
|
+
body = []
|
|
1073
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1074
|
+
while self.check('NEWLINE'): self.consume()
|
|
1075
|
+
if self.check('DEDENT'): break
|
|
1076
|
+
body.append(self.parse_statement())
|
|
1077
|
+
self.consume('DEDENT')
|
|
1078
|
+
node = While(condition, body)
|
|
1079
|
+
node.line = start_token.line
|
|
1080
|
+
return node
|
|
1081
|
+
|
|
1082
|
+
def parse_repeat(self) -> Repeat:
|
|
1083
|
+
start_token = self.consume('REPEAT')
|
|
1084
|
+
count = self.parse_expression()
|
|
1085
|
+
self.consume('TIMES')
|
|
1086
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1087
|
+
self.consume('NEWLINE')
|
|
1088
|
+
self.consume('INDENT')
|
|
1089
|
+
body = []
|
|
1090
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1091
|
+
while self.check('NEWLINE'): self.consume()
|
|
1092
|
+
if self.check('DEDENT'): break
|
|
1093
|
+
body.append(self.parse_statement())
|
|
1094
|
+
self.consume('DEDENT')
|
|
1095
|
+
node = Repeat(count, body)
|
|
1096
|
+
node.line = start_token.line
|
|
1097
|
+
return node
|
|
1098
|
+
|
|
1099
|
+
def parse_try(self) -> Try:
|
|
1100
|
+
start_token = self.consume('TRY')
|
|
1101
|
+
if self.check('COLON'):
|
|
1102
|
+
self.consume('COLON')
|
|
1103
|
+
self.consume('NEWLINE')
|
|
1104
|
+
self.consume('INDENT')
|
|
1105
|
+
try_body = []
|
|
1106
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1107
|
+
while self.check('NEWLINE'): self.consume()
|
|
1108
|
+
if self.check('DEDENT'): break
|
|
1109
|
+
try_body.append(self.parse_statement())
|
|
1110
|
+
self.consume('DEDENT')
|
|
1111
|
+
self.consume('CATCH')
|
|
1112
|
+
catch_var = self.consume('ID').value
|
|
1113
|
+
if self.check('COLON'):
|
|
1114
|
+
self.consume('COLON')
|
|
1115
|
+
self.consume('NEWLINE')
|
|
1116
|
+
self.consume('INDENT')
|
|
1117
|
+
catch_body = []
|
|
1118
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1119
|
+
while self.check('NEWLINE'): self.consume()
|
|
1120
|
+
if self.check('DEDENT'): break
|
|
1121
|
+
catch_body.append(self.parse_statement())
|
|
1122
|
+
self.consume('DEDENT')
|
|
1123
|
+
always_body = []
|
|
1124
|
+
if self.check('ALWAYS'):
|
|
1125
|
+
self.consume('ALWAYS')
|
|
1126
|
+
self.consume('NEWLINE')
|
|
1127
|
+
self.consume('INDENT')
|
|
1128
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1129
|
+
while self.check('NEWLINE'): self.consume()
|
|
1130
|
+
if self.check('DEDENT'): break
|
|
1131
|
+
always_body.append(self.parse_statement())
|
|
1132
|
+
self.consume('DEDENT')
|
|
1133
|
+
if always_body:
|
|
1134
|
+
node = TryAlways(try_body, catch_var, catch_body, always_body)
|
|
1135
|
+
else:
|
|
1136
|
+
node = Try(try_body, catch_var, catch_body)
|
|
1137
|
+
node.line = start_token.line
|
|
1138
|
+
return node
|
|
1139
|
+
def parse_list(self) -> Node:
|
|
1140
|
+
token = self.consume('LBRACKET')
|
|
1141
|
+
def skip_formatted():
|
|
1142
|
+
while self.check('NEWLINE') or self.check('INDENT') or self.check('DEDENT'):
|
|
1143
|
+
self.consume()
|
|
1144
|
+
skip_formatted()
|
|
1145
|
+
if self.check('RBRACKET'):
|
|
1146
|
+
self.consume('RBRACKET')
|
|
1147
|
+
node = ListVal([])
|
|
1148
|
+
node.line = token.line
|
|
1149
|
+
return node
|
|
1150
|
+
if self.check('DOTDOTDOT'):
|
|
1151
|
+
node = self._parse_list_with_spread(token)
|
|
1152
|
+
skip_formatted()
|
|
1153
|
+
return node
|
|
1154
|
+
first_expr = self.parse_expression()
|
|
1155
|
+
skip_formatted()
|
|
1156
|
+
|
|
1157
|
+
if self.check('TO'):
|
|
1158
|
+
self.consume('TO')
|
|
1159
|
+
end_val = self.parse_expression()
|
|
1160
|
+
skip_formatted()
|
|
1161
|
+
self.consume('RBRACKET')
|
|
1162
|
+
node = Call('range', [first_expr, end_val])
|
|
1163
|
+
node.line = token.line
|
|
1164
|
+
return node
|
|
1165
|
+
|
|
1166
|
+
if self.check('FOR'):
|
|
1167
|
+
self.consume('FOR')
|
|
1168
|
+
var_name = self.consume('ID').value
|
|
1169
|
+
self.consume('IN')
|
|
1170
|
+
iterable = self.parse_expression()
|
|
1171
|
+
condition = None
|
|
1172
|
+
if self.check('IF'):
|
|
1173
|
+
self.consume('IF')
|
|
1174
|
+
condition = self.parse_expression()
|
|
1175
|
+
self.consume('RBRACKET')
|
|
1176
|
+
node = ListComprehension(first_expr, var_name, iterable, condition)
|
|
1177
|
+
node.line = token.line
|
|
1178
|
+
return node
|
|
1179
|
+
elements = [first_expr]
|
|
1180
|
+
while self.check('COMMA'):
|
|
1181
|
+
self.consume('COMMA')
|
|
1182
|
+
skip_formatted()
|
|
1183
|
+
if self.check('RBRACKET'):
|
|
1184
|
+
break
|
|
1185
|
+
if self.check('DOTDOTDOT'):
|
|
1186
|
+
self.consume('DOTDOTDOT')
|
|
1187
|
+
spread_val = self.parse_expression()
|
|
1188
|
+
spread_node = Spread(spread_val)
|
|
1189
|
+
spread_node.line = token.line
|
|
1190
|
+
elements.append(spread_node)
|
|
1191
|
+
else:
|
|
1192
|
+
elements.append(self.parse_expression())
|
|
1193
|
+
skip_formatted()
|
|
1194
|
+
skip_formatted()
|
|
1195
|
+
self.consume('RBRACKET')
|
|
1196
|
+
node = ListVal(elements)
|
|
1197
|
+
node.line = token.line
|
|
1198
|
+
return node
|
|
1199
|
+
def _parse_list_with_spread(self, token: Token) -> ListVal:
|
|
1200
|
+
elements = []
|
|
1201
|
+
self.consume('DOTDOTDOT')
|
|
1202
|
+
spread_val = self.parse_expression()
|
|
1203
|
+
spread_node = Spread(spread_val)
|
|
1204
|
+
spread_node.line = token.line
|
|
1205
|
+
elements.append(spread_node)
|
|
1206
|
+
while self.check('COMMA'):
|
|
1207
|
+
self.consume('COMMA')
|
|
1208
|
+
if self.check('RBRACKET'):
|
|
1209
|
+
break
|
|
1210
|
+
if self.check('DOTDOTDOT'):
|
|
1211
|
+
self.consume('DOTDOTDOT')
|
|
1212
|
+
spread_val = self.parse_expression()
|
|
1213
|
+
spread_node = Spread(spread_val)
|
|
1214
|
+
spread_node.line = token.line
|
|
1215
|
+
elements.append(spread_node)
|
|
1216
|
+
else:
|
|
1217
|
+
elements.append(self.parse_expression())
|
|
1218
|
+
self.consume('RBRACKET')
|
|
1219
|
+
node = ListVal(elements)
|
|
1220
|
+
node.line = token.line
|
|
1221
|
+
return node
|
|
1222
|
+
def parse_dict(self) -> Dictionary:
|
|
1223
|
+
token = self.consume('LBRACE')
|
|
1224
|
+
def skip_formatted():
|
|
1225
|
+
while self.check('NEWLINE') or self.check('INDENT') or self.check('DEDENT'):
|
|
1226
|
+
self.consume()
|
|
1227
|
+
skip_formatted()
|
|
1228
|
+
pairs = []
|
|
1229
|
+
if not self.check('RBRACE'):
|
|
1230
|
+
if self.check('ID') and self.peek(1).type == 'COLON':
|
|
1231
|
+
key_token = self.consume('ID')
|
|
1232
|
+
key = String(key_token.value)
|
|
1233
|
+
key.line = key_token.line
|
|
1234
|
+
else:
|
|
1235
|
+
key = self.parse_expression()
|
|
1236
|
+
self.consume('COLON')
|
|
1237
|
+
skip_formatted()
|
|
1238
|
+
value = self.parse_expression()
|
|
1239
|
+
pairs.append((key, value))
|
|
1240
|
+
skip_formatted()
|
|
1241
|
+
while self.check('COMMA'):
|
|
1242
|
+
self.consume('COMMA')
|
|
1243
|
+
skip_formatted()
|
|
1244
|
+
if self.check('RBRACE'): break
|
|
1245
|
+
if self.check('ID') and self.peek(1).type == 'COLON':
|
|
1246
|
+
key_token = self.consume('ID')
|
|
1247
|
+
key = String(key_token.value)
|
|
1248
|
+
key.line = key_token.line
|
|
1249
|
+
else:
|
|
1250
|
+
key = self.parse_expression()
|
|
1251
|
+
self.consume('COLON')
|
|
1252
|
+
skip_formatted()
|
|
1253
|
+
value = self.parse_expression()
|
|
1254
|
+
pairs.append((key, value))
|
|
1255
|
+
skip_formatted()
|
|
1256
|
+
skip_formatted()
|
|
1257
|
+
self.consume('RBRACE')
|
|
1258
|
+
node = Dictionary(pairs)
|
|
1259
|
+
node.line = token.line
|
|
1260
|
+
return node
|
|
1261
|
+
def parse_app(self) -> Node:
|
|
1262
|
+
token = self.consume('APP')
|
|
1263
|
+
title = self.consume('STRING').value
|
|
1264
|
+
|
|
1265
|
+
width = 500
|
|
1266
|
+
height = 400
|
|
1267
|
+
if self.check('SIZE'):
|
|
1268
|
+
self.consume('SIZE')
|
|
1269
|
+
width = int(self.consume('NUMBER').value)
|
|
1270
|
+
height = int(self.consume('NUMBER').value)
|
|
1271
|
+
|
|
1272
|
+
self.consume('COLON')
|
|
1273
|
+
self.consume('NEWLINE')
|
|
1274
|
+
self.consume('INDENT')
|
|
1275
|
+
|
|
1276
|
+
body = []
|
|
1277
|
+
while not self.check('DEDENT'):
|
|
1278
|
+
body.append(self.parse_ui_block())
|
|
1279
|
+
if self.check('NEWLINE'):
|
|
1280
|
+
self.consume('NEWLINE')
|
|
1281
|
+
|
|
1282
|
+
self.consume('DEDENT')
|
|
1283
|
+
return App(title, width, height, body)
|
|
1284
|
+
|
|
1285
|
+
def parse_ui_block(self) -> Node:
|
|
1286
|
+
token = self.peek()
|
|
1287
|
+
|
|
1288
|
+
if token.type in ('COLUMN', 'ROW'):
|
|
1289
|
+
layout_type = self.consume().value # column or row
|
|
1290
|
+
self.consume('COLON')
|
|
1291
|
+
self.consume('NEWLINE')
|
|
1292
|
+
self.consume('INDENT')
|
|
1293
|
+
children = []
|
|
1294
|
+
while not self.check('DEDENT'):
|
|
1295
|
+
children.append(self.parse_ui_block())
|
|
1296
|
+
if self.check('NEWLINE'): self.consume('NEWLINE')
|
|
1297
|
+
self.consume('DEDENT')
|
|
1298
|
+
return Layout(layout_type, children)
|
|
1299
|
+
|
|
1300
|
+
elif (token.type in ('BUTTON', 'INPUT', 'HEADING') or
|
|
1301
|
+
(token.type == 'ID' and token.value == 'text')):
|
|
1302
|
+
if token.type == 'ID' and token.value == 'text':
|
|
1303
|
+
widget_type = 'TEXT'
|
|
1304
|
+
self.consume() # consume 'text' ID
|
|
1305
|
+
else:
|
|
1306
|
+
widget_type = self.consume().value
|
|
1307
|
+
label = self.consume('STRING').value
|
|
1308
|
+
|
|
1309
|
+
var_name = None
|
|
1310
|
+
if self.check('AS'):
|
|
1311
|
+
self.consume('AS')
|
|
1312
|
+
var_name = self.consume('ID').value
|
|
1313
|
+
|
|
1314
|
+
event_handler = None
|
|
1315
|
+
if token.type == 'BUTTON' and self.check('DO'):
|
|
1316
|
+
self.consume('DO')
|
|
1317
|
+
self.consume('COLON')
|
|
1318
|
+
self.consume('NEWLINE')
|
|
1319
|
+
self.consume('INDENT')
|
|
1320
|
+
event_handler = []
|
|
1321
|
+
while not self.check('DEDENT'):
|
|
1322
|
+
event_handler.append(self.parse_statement())
|
|
1323
|
+
self.consume('DEDENT')
|
|
1324
|
+
|
|
1325
|
+
return Widget(widget_type, label, var_name, event_handler)
|
|
1326
|
+
|
|
1327
|
+
else:
|
|
1328
|
+
raise SyntaxError(f"Unexpected token in UI block: {token.type} at line {token.line}")
|
|
1329
|
+
|
|
1330
|
+
def parse_factor_simple(self) -> Node:
|
|
1331
|
+
token = self.peek()
|
|
1332
|
+
if token.type == 'ASK':
|
|
1333
|
+
self.consume('ASK')
|
|
1334
|
+
prompt = self.parse_expression()
|
|
1335
|
+
node = Call('input', [prompt]) # Alias to input
|
|
1336
|
+
node.line = token.line
|
|
1337
|
+
return node
|
|
1338
|
+
elif token.type == 'NUMBER':
|
|
1339
|
+
self.consume()
|
|
1340
|
+
val = token.value
|
|
1341
|
+
if '.' in val:
|
|
1342
|
+
node = Number(float(val))
|
|
1343
|
+
else:
|
|
1344
|
+
node = Number(int(val))
|
|
1345
|
+
node.line = token.line
|
|
1346
|
+
return node
|
|
1347
|
+
elif token.type == 'STRING':
|
|
1348
|
+
self.consume()
|
|
1349
|
+
val = token.value
|
|
1350
|
+
if '{' in val and '}' in val:
|
|
1351
|
+
parts = re.split(r'\{([^}]+)\}', val)
|
|
1352
|
+
if len(parts) == 1:
|
|
1353
|
+
node = String(val)
|
|
1354
|
+
node.line = token.line
|
|
1355
|
+
return node
|
|
1356
|
+
current_node = None
|
|
1357
|
+
for i, part in enumerate(parts):
|
|
1358
|
+
if i % 2 == 0:
|
|
1359
|
+
if not part: continue
|
|
1360
|
+
expr = String(part)
|
|
1361
|
+
expr.line = token.line
|
|
1362
|
+
else:
|
|
1363
|
+
snippet = part.strip()
|
|
1364
|
+
if snippet:
|
|
1365
|
+
sub_lexer = Lexer(snippet)
|
|
1366
|
+
sub_tokens = sub_lexer.tokenize()
|
|
1367
|
+
sub_parser = Parser(sub_tokens)
|
|
1368
|
+
try:
|
|
1369
|
+
expr = sub_parser.parse_expression()
|
|
1370
|
+
expr.line = token.line
|
|
1371
|
+
except Exception as e:
|
|
1372
|
+
raise SyntaxError(f"Invalid interpolation expression: '{snippet}' on line {token.line}")
|
|
1373
|
+
else:
|
|
1374
|
+
continue
|
|
1375
|
+
if current_node is None:
|
|
1376
|
+
current_node = expr
|
|
1377
|
+
else:
|
|
1378
|
+
current_node = BinOp(current_node, '+', expr)
|
|
1379
|
+
current_node.line = token.line
|
|
1380
|
+
return current_node if current_node else String("")
|
|
1381
|
+
node = String(token.value)
|
|
1382
|
+
node.line = token.line
|
|
1383
|
+
return node
|
|
1384
|
+
elif token.type == 'YES':
|
|
1385
|
+
self.consume()
|
|
1386
|
+
node = Boolean(True)
|
|
1387
|
+
node.line = token.line
|
|
1388
|
+
return node
|
|
1389
|
+
elif token.type == 'NO':
|
|
1390
|
+
self.consume()
|
|
1391
|
+
node = Boolean(False)
|
|
1392
|
+
node.line = token.line
|
|
1393
|
+
return node
|
|
1394
|
+
elif token.type == 'LBRACKET':
|
|
1395
|
+
return self.parse_list()
|
|
1396
|
+
elif token.type == 'LBRACE':
|
|
1397
|
+
return self.parse_dict()
|
|
1398
|
+
elif token.type == 'ID':
|
|
1399
|
+
self.consume()
|
|
1400
|
+
if self.check('DOT'):
|
|
1401
|
+
self.consume('DOT')
|
|
1402
|
+
prop_token = self.consume()
|
|
1403
|
+
prop = prop_token.value
|
|
1404
|
+
node = PropertyAccess(token.value, prop)
|
|
1405
|
+
node.line = token.line
|
|
1406
|
+
return node
|
|
1407
|
+
node = VarAccess(token.value)
|
|
1408
|
+
node.line = token.line
|
|
1409
|
+
return node
|
|
1410
|
+
elif token.type == 'LPAREN':
|
|
1411
|
+
self.consume()
|
|
1412
|
+
expr = self.parse_expression()
|
|
1413
|
+
self.consume('RPAREN')
|
|
1414
|
+
return expr
|
|
1415
|
+
elif token.type == 'INPUT':
|
|
1416
|
+
is_tag = False
|
|
1417
|
+
next_t = self.peek(1)
|
|
1418
|
+
if next_t.type in ('ID', 'TYPE', 'STRING', 'NAME', 'VALUE', 'CLASS', 'STYLE', 'ONCLICK', 'SRC', 'HREF', 'ACTION', 'METHOD'):
|
|
1419
|
+
is_tag = True
|
|
1420
|
+
if is_tag:
|
|
1421
|
+
return self.parse_id_start_statement(passed_name_token=token)
|
|
1422
|
+
self.consume()
|
|
1423
|
+
prompt = None
|
|
1424
|
+
if self.check('STRING'):
|
|
1425
|
+
prompt = self.consume('STRING').value
|
|
1426
|
+
node = Input(prompt)
|
|
1427
|
+
node.line = token.line
|
|
1428
|
+
return node
|
|
1429
|
+
raise SyntaxError(f"Unexpected argument token {token.type} at line {token.line}")
|
|
1430
|
+
def parse_factor(self) -> Node:
|
|
1431
|
+
token = self.peek()
|
|
1432
|
+
if token.type == 'MINUS':
|
|
1433
|
+
op = self.consume()
|
|
1434
|
+
right = self.parse_factor()
|
|
1435
|
+
node = UnaryOp('-', right)
|
|
1436
|
+
node.line = op.line
|
|
1437
|
+
return node
|
|
1438
|
+
elif token.type == 'NOT':
|
|
1439
|
+
op = self.consume()
|
|
1440
|
+
right = self.parse_factor()
|
|
1441
|
+
node = UnaryOp('not', right)
|
|
1442
|
+
node.line = op.line
|
|
1443
|
+
return node
|
|
1444
|
+
elif token.type == 'ASK':
|
|
1445
|
+
return self.parse_factor_simple()
|
|
1446
|
+
elif token.type == 'LPAREN':
|
|
1447
|
+
self.consume('LPAREN')
|
|
1448
|
+
node = self.parse_expression()
|
|
1449
|
+
self.consume('RPAREN')
|
|
1450
|
+
return node
|
|
1451
|
+
elif token.type == 'DB':
|
|
1452
|
+
return self.parse_db_op()
|
|
1453
|
+
elif token.type == 'SPAWN':
|
|
1454
|
+
op = self.consume()
|
|
1455
|
+
right = self.parse_factor()
|
|
1456
|
+
node = Spawn(right)
|
|
1457
|
+
node.line = op.line
|
|
1458
|
+
return node
|
|
1459
|
+
elif token.type == 'EXECUTE':
|
|
1460
|
+
op = self.consume()
|
|
1461
|
+
right = self.parse_expression()
|
|
1462
|
+
node = Call('run', [right])
|
|
1463
|
+
node.line = op.line
|
|
1464
|
+
return node
|
|
1465
|
+
elif token.type == 'COUNT' or token.type == 'HOW':
|
|
1466
|
+
token = self.consume()
|
|
1467
|
+
if token.type == 'HOW':
|
|
1468
|
+
self.consume('MANY')
|
|
1469
|
+
if self.check('OF'):
|
|
1470
|
+
self.consume('OF')
|
|
1471
|
+
expr = self.parse_expression()
|
|
1472
|
+
node = Call('len', [expr])
|
|
1473
|
+
node.line = token.line
|
|
1474
|
+
return node
|
|
1475
|
+
elif token.type == 'AWAIT':
|
|
1476
|
+
op = self.consume()
|
|
1477
|
+
right = self.parse_factor()
|
|
1478
|
+
node = Await(right)
|
|
1479
|
+
node.line = op.line
|
|
1480
|
+
return node
|
|
1481
|
+
elif token.type == 'CONVERT':
|
|
1482
|
+
return self.parse_convert()
|
|
1483
|
+
elif token.type == 'LOAD' and self.peek(1).type == 'CSV':
|
|
1484
|
+
self.consume('LOAD')
|
|
1485
|
+
self.consume('CSV')
|
|
1486
|
+
path = self.parse_factor()
|
|
1487
|
+
node = CsvOp('load', None, path)
|
|
1488
|
+
node.line = token.line
|
|
1489
|
+
return node
|
|
1490
|
+
elif token.type == 'PASTE':
|
|
1491
|
+
token = self.consume('PASTE')
|
|
1492
|
+
self.consume('FROM')
|
|
1493
|
+
self.consume('CLIPBOARD')
|
|
1494
|
+
node = ClipboardOp('paste', None)
|
|
1495
|
+
node.line = token.line
|
|
1496
|
+
return node
|
|
1497
|
+
elif token.type == 'READ':
|
|
1498
|
+
token = self.consume('READ')
|
|
1499
|
+
if self.check('FILE'):
|
|
1500
|
+
self.consume('FILE')
|
|
1501
|
+
path = self.parse_factor()
|
|
1502
|
+
node = FileRead(path)
|
|
1503
|
+
node.line = token.line
|
|
1504
|
+
return node
|
|
1505
|
+
elif token.type == 'SUM':
|
|
1506
|
+
return self.parse_sum()
|
|
1507
|
+
elif token.type == 'UPPER':
|
|
1508
|
+
return self.parse_upper()
|
|
1509
|
+
elif token.type == 'DATE':
|
|
1510
|
+
token = self.consume('DATE')
|
|
1511
|
+
s = self.consume('STRING').value
|
|
1512
|
+
node = DateOp(s)
|
|
1513
|
+
node.line = token.line
|
|
1514
|
+
return node
|
|
1515
|
+
elif token.type == 'TODAY':
|
|
1516
|
+
token = self.consume('TODAY')
|
|
1517
|
+
node = DateOp('today')
|
|
1518
|
+
node.line = token.line
|
|
1519
|
+
return node
|
|
1520
|
+
if token.type == 'NUMBER':
|
|
1521
|
+
self.consume()
|
|
1522
|
+
val = token.value
|
|
1523
|
+
if '.' in val:
|
|
1524
|
+
node = Number(float(val))
|
|
1525
|
+
else:
|
|
1526
|
+
node = Number(int(val))
|
|
1527
|
+
node.line = token.line
|
|
1528
|
+
return node
|
|
1529
|
+
elif token.type == 'REGEX':
|
|
1530
|
+
self.consume()
|
|
1531
|
+
node = Regex(token.value)
|
|
1532
|
+
node.line = token.line
|
|
1533
|
+
return node
|
|
1534
|
+
elif token.type == 'STRING':
|
|
1535
|
+
return self.parse_factor_simple()
|
|
1536
|
+
elif token.type == 'YES':
|
|
1537
|
+
self.consume()
|
|
1538
|
+
node = Boolean(True)
|
|
1539
|
+
node.line = token.line
|
|
1540
|
+
return node
|
|
1541
|
+
elif token.type == 'NO':
|
|
1542
|
+
self.consume()
|
|
1543
|
+
node = Boolean(False)
|
|
1544
|
+
node.line = token.line
|
|
1545
|
+
return node
|
|
1546
|
+
elif token.type == 'LBRACKET':
|
|
1547
|
+
return self.parse_list()
|
|
1548
|
+
elif token.type == 'LBRACE':
|
|
1549
|
+
return self.parse_dict()
|
|
1550
|
+
elif token.type == 'ID':
|
|
1551
|
+
if token.value == 'a':
|
|
1552
|
+
if self.peek(1).type == 'LIST' and self.peek(2).type == 'OF':
|
|
1553
|
+
return self._parse_natural_list()
|
|
1554
|
+
elif self.peek(1).type == 'UNIQUE' and self.peek(2).type == 'SET' and self.peek(3).type == 'OF':
|
|
1555
|
+
return self._parse_natural_set()
|
|
1556
|
+
if token.value == 'numbers' and self.peek(1).type == 'FROM':
|
|
1557
|
+
return self.parse_numbers_range()
|
|
1558
|
+
self.consume()
|
|
1559
|
+
instance_name = token.value
|
|
1560
|
+
method_name = None
|
|
1561
|
+
if self.check('DOT'):
|
|
1562
|
+
self.consume('DOT')
|
|
1563
|
+
method_name = self.consume().value
|
|
1564
|
+
|
|
1565
|
+
args = []
|
|
1566
|
+
# Check for C-style function call: func(arg1, arg2)
|
|
1567
|
+
if self.check('LPAREN'):
|
|
1568
|
+
self.consume('LPAREN')
|
|
1569
|
+
if not self.check('RPAREN'):
|
|
1570
|
+
args.append(self.parse_expression())
|
|
1571
|
+
while self.check('COMMA'):
|
|
1572
|
+
self.consume('COMMA')
|
|
1573
|
+
args.append(self.parse_expression())
|
|
1574
|
+
self.consume('RPAREN')
|
|
1575
|
+
if method_name:
|
|
1576
|
+
node = MethodCall(instance_name, method_name, args)
|
|
1577
|
+
else:
|
|
1578
|
+
node = Call(instance_name, args)
|
|
1579
|
+
node.line = token.line
|
|
1580
|
+
return node
|
|
1581
|
+
|
|
1582
|
+
# Fallback to command-style arguments: func arg1 arg2
|
|
1583
|
+
# But only if NOT a property access (method_name w/o args is property access,
|
|
1584
|
+
# but if it was method call it would use parens ideally, or spaces?)
|
|
1585
|
+
# Existing logic supported 'func arg1 arg2'.
|
|
1586
|
+
# We keep it for backward compatibility e.g. 'echo 123'.
|
|
1587
|
+
|
|
1588
|
+
force_call = False
|
|
1589
|
+
while True:
|
|
1590
|
+
next_t = self.peek()
|
|
1591
|
+
# If we see LPAREN now, it's ambiguous if we didn't handle it above.
|
|
1592
|
+
# But 'func (1)' is func called with 1 arg which is (1).
|
|
1593
|
+
# 'func (1, 2)' would fail in old logic too.
|
|
1594
|
+
# So the above block handles the explicit invocation.
|
|
1595
|
+
# This loop is for 'func 1 2'.
|
|
1596
|
+
|
|
1597
|
+
# Check for keywords or invalid start tokens
|
|
1598
|
+
if next_t.type not in ('NUMBER', 'STRING', 'REGEX', 'ID', 'LPAREN', 'INPUT', 'ASK', 'YES', 'NO', 'LBRACKET', 'LBRACE'):
|
|
1599
|
+
break
|
|
1600
|
+
|
|
1601
|
+
# Special case: if we see LPAREN, is it part of command args?
|
|
1602
|
+
# e.g. 'func (1+2)' -> args=[(1+2)].
|
|
1603
|
+
args.append(self.parse_factor_simple())
|
|
1604
|
+
|
|
1605
|
+
if method_name:
|
|
1606
|
+
if args:
|
|
1607
|
+
node = MethodCall(instance_name, method_name, args)
|
|
1608
|
+
else:
|
|
1609
|
+
node = PropertyAccess(instance_name, method_name)
|
|
1610
|
+
node.line = token.line
|
|
1611
|
+
return node
|
|
1612
|
+
|
|
1613
|
+
if args:
|
|
1614
|
+
node = Call(instance_name, args)
|
|
1615
|
+
node.line = token.line
|
|
1616
|
+
return node
|
|
1617
|
+
|
|
1618
|
+
node = VarAccess(instance_name)
|
|
1619
|
+
node.line = token.line
|
|
1620
|
+
return node
|
|
1621
|
+
elif token.type == 'LPAREN':
|
|
1622
|
+
self.consume()
|
|
1623
|
+
expr = self.parse_expression()
|
|
1624
|
+
self.consume('RPAREN')
|
|
1625
|
+
return expr
|
|
1626
|
+
elif token.type == 'INPUT' or token.type == 'ASK':
|
|
1627
|
+
next_t = self.peek(1)
|
|
1628
|
+
if next_t.type in ('ID', 'TYPE', 'STRING', 'NAME', 'VALUE', 'CLASS', 'STYLE', 'ONCLICK', 'SRC', 'HREF', 'ACTION', 'METHOD'):
|
|
1629
|
+
self.consume()
|
|
1630
|
+
return self.parse_id_start_statement(passed_name_token=token)
|
|
1631
|
+
self.consume()
|
|
1632
|
+
prompt = None
|
|
1633
|
+
if self.check('STRING'):
|
|
1634
|
+
prompt = self.consume('STRING').value
|
|
1635
|
+
node = Input(prompt)
|
|
1636
|
+
node.line = token.line
|
|
1637
|
+
return node
|
|
1638
|
+
elif token.type == 'PROMPT':
|
|
1639
|
+
self.consume()
|
|
1640
|
+
prompt_expr = self.parse_factor()
|
|
1641
|
+
node = Prompt(prompt_expr)
|
|
1642
|
+
node.line = token.line
|
|
1643
|
+
return node
|
|
1644
|
+
elif token.type == 'CONFIRM':
|
|
1645
|
+
self.consume()
|
|
1646
|
+
prompt_expr = self.parse_factor()
|
|
1647
|
+
node = Confirm(prompt_expr)
|
|
1648
|
+
node.line = token.line
|
|
1649
|
+
return node
|
|
1650
|
+
elif token.type == 'MAKE':
|
|
1651
|
+
return self.parse_make_expr()
|
|
1652
|
+
raise SyntaxError(f"Unexpected token {token.type} at line {token.line}")
|
|
1653
|
+
def parse_for(self) -> Node:
|
|
1654
|
+
if self.check('LOOP'):
|
|
1655
|
+
start_token = self.consume('LOOP')
|
|
1656
|
+
count_expr = self.parse_expression()
|
|
1657
|
+
self.consume('TIMES')
|
|
1658
|
+
self.consume('NEWLINE')
|
|
1659
|
+
self.consume('INDENT')
|
|
1660
|
+
body = []
|
|
1661
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1662
|
+
while self.check('NEWLINE'): self.consume()
|
|
1663
|
+
if self.check('DEDENT'): break
|
|
1664
|
+
body.append(self.parse_statement())
|
|
1665
|
+
self.consume('DEDENT')
|
|
1666
|
+
node = For(count_expr, body)
|
|
1667
|
+
node.line = start_token.line
|
|
1668
|
+
return node
|
|
1669
|
+
start_token = self.consume('FOR')
|
|
1670
|
+
if self.check('ID') and self.peek(1).type == 'IN':
|
|
1671
|
+
var_name = self.consume('ID').value
|
|
1672
|
+
self.consume('IN')
|
|
1673
|
+
if self.check('RANGE'):
|
|
1674
|
+
self.consume('RANGE')
|
|
1675
|
+
start_val = self.parse_expression()
|
|
1676
|
+
end_val = self.parse_expression()
|
|
1677
|
+
self.consume('NEWLINE')
|
|
1678
|
+
self.consume('INDENT')
|
|
1679
|
+
body = []
|
|
1680
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1681
|
+
while self.check('NEWLINE'): self.consume()
|
|
1682
|
+
if self.check('DEDENT'): break
|
|
1683
|
+
body.append(self.parse_statement())
|
|
1684
|
+
self.consume('DEDENT')
|
|
1685
|
+
iterable = Call('range', [start_val, end_val])
|
|
1686
|
+
node = ForIn(var_name, iterable, body)
|
|
1687
|
+
node.line = start_token.line
|
|
1688
|
+
return node
|
|
1689
|
+
else:
|
|
1690
|
+
iterable = self.parse_expression()
|
|
1691
|
+
self.consume('NEWLINE')
|
|
1692
|
+
self.consume('INDENT')
|
|
1693
|
+
body = []
|
|
1694
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1695
|
+
while self.check('NEWLINE'): self.consume()
|
|
1696
|
+
if self.check('DEDENT'): break
|
|
1697
|
+
body.append(self.parse_statement())
|
|
1698
|
+
self.consume('DEDENT')
|
|
1699
|
+
node = ForIn(var_name, iterable, body)
|
|
1700
|
+
node.line = start_token.line
|
|
1701
|
+
return node
|
|
1702
|
+
else:
|
|
1703
|
+
count_expr = self.parse_expression()
|
|
1704
|
+
self.consume('IN')
|
|
1705
|
+
self.consume('RANGE')
|
|
1706
|
+
if self.check('COLON'):
|
|
1707
|
+
self.consume('COLON')
|
|
1708
|
+
self.consume('NEWLINE')
|
|
1709
|
+
self.consume('INDENT')
|
|
1710
|
+
body = []
|
|
1711
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1712
|
+
while self.check('NEWLINE'): self.consume()
|
|
1713
|
+
if self.check('DEDENT'): break
|
|
1714
|
+
body.append(self.parse_statement())
|
|
1715
|
+
self.consume('DEDENT')
|
|
1716
|
+
node = For(count_expr, body)
|
|
1717
|
+
node.line = start_token.line
|
|
1718
|
+
return node
|
|
1719
|
+
def parse_expression_stmt(self) -> Node:
|
|
1720
|
+
expr = self.parse_expression()
|
|
1721
|
+
self.consume('NEWLINE')
|
|
1722
|
+
node = Print(expression=expr)
|
|
1723
|
+
node.line = expr.line
|
|
1724
|
+
return node
|
|
1725
|
+
def parse_expression(self) -> Node:
|
|
1726
|
+
if self.check('FN'):
|
|
1727
|
+
return self.parse_lambda()
|
|
1728
|
+
return self.parse_ternary()
|
|
1729
|
+
def parse_lambda(self) -> Lambda:
|
|
1730
|
+
token = self.consume('FN')
|
|
1731
|
+
params = []
|
|
1732
|
+
while self.check('ID'):
|
|
1733
|
+
params.append(self.consume('ID').value)
|
|
1734
|
+
self.consume('ARROW')
|
|
1735
|
+
body = self.parse_expression()
|
|
1736
|
+
node = Lambda(params, body)
|
|
1737
|
+
node.line = token.line
|
|
1738
|
+
return node
|
|
1739
|
+
def parse_ternary(self) -> Node:
|
|
1740
|
+
condition = self.parse_logic_or()
|
|
1741
|
+
if self.check('QUESTION'):
|
|
1742
|
+
self.consume('QUESTION')
|
|
1743
|
+
true_expr = self.parse_expression()
|
|
1744
|
+
self.consume('COLON')
|
|
1745
|
+
false_expr = self.parse_expression()
|
|
1746
|
+
node = Ternary(condition, true_expr, false_expr)
|
|
1747
|
+
node.line = condition.line
|
|
1748
|
+
return node
|
|
1749
|
+
return condition
|
|
1750
|
+
def parse_logic_or(self) -> Node:
|
|
1751
|
+
left = self.parse_logic_and()
|
|
1752
|
+
while self.check('OR'):
|
|
1753
|
+
op_token = self.consume()
|
|
1754
|
+
right = self.parse_logic_and()
|
|
1755
|
+
new_node = BinOp(left, op_token.value, right)
|
|
1756
|
+
new_node.line = op_token.line
|
|
1757
|
+
left = new_node
|
|
1758
|
+
return left
|
|
1759
|
+
def parse_logic_and(self) -> Node:
|
|
1760
|
+
left = self.parse_comparison()
|
|
1761
|
+
while self.check('AND'):
|
|
1762
|
+
op_token = self.consume()
|
|
1763
|
+
right = self.parse_comparison()
|
|
1764
|
+
new_node = BinOp(left, op_token.value, right)
|
|
1765
|
+
new_node.line = op_token.line
|
|
1766
|
+
left = new_node
|
|
1767
|
+
return left
|
|
1768
|
+
def parse_comparison(self) -> Node:
|
|
1769
|
+
left = self.parse_arithmetic()
|
|
1770
|
+
if self.peek().type in ('EQ', 'NEQ', 'GT', 'LT', 'GE', 'LE', 'IS', 'MATCHES', 'GREATER', 'LESS', 'EQUAL', 'CONTAINS', 'EMPTY'):
|
|
1771
|
+
op_token = self.consume()
|
|
1772
|
+
op_val = op_token.value
|
|
1773
|
+
|
|
1774
|
+
# Handle "is greater/less/equal"
|
|
1775
|
+
if op_token.type == 'IS':
|
|
1776
|
+
if self.check('GREATER'):
|
|
1777
|
+
self.consume('GREATER'); self.consume('THAN')
|
|
1778
|
+
op_val = '>'
|
|
1779
|
+
elif self.check('LESS'):
|
|
1780
|
+
self.consume('LESS'); self.consume('THAN')
|
|
1781
|
+
op_val = '<'
|
|
1782
|
+
elif self.check('EQUAL'):
|
|
1783
|
+
self.consume('EQUAL'); self.consume('TO')
|
|
1784
|
+
op_val = '=='
|
|
1785
|
+
elif self.check('NOT'):
|
|
1786
|
+
self.consume('NOT'); self.consume('EQUAL'); self.consume('TO')
|
|
1787
|
+
op_val = '!='
|
|
1788
|
+
elif self.check('EMPTY'):
|
|
1789
|
+
self.consume('EMPTY')
|
|
1790
|
+
# is empty -> Call('empty', [left])
|
|
1791
|
+
node = Call('empty', [left])
|
|
1792
|
+
node.line = op_token.line
|
|
1793
|
+
return node
|
|
1794
|
+
else:
|
|
1795
|
+
op_val = '=='
|
|
1796
|
+
elif op_token.type == 'CONTAINS':
|
|
1797
|
+
# list contains item -> Call('contains', [list, item])
|
|
1798
|
+
right = self.parse_arithmetic()
|
|
1799
|
+
node = Call('contains', [left, right])
|
|
1800
|
+
node.line = op_token.line
|
|
1801
|
+
return node
|
|
1802
|
+
|
|
1803
|
+
right = self.parse_arithmetic()
|
|
1804
|
+
node = BinOp(left, op_val, right)
|
|
1805
|
+
node.line = op_token.line
|
|
1806
|
+
return node
|
|
1807
|
+
return left
|
|
1808
|
+
def parse_arithmetic(self) -> Node:
|
|
1809
|
+
left = self.parse_term()
|
|
1810
|
+
while self.peek().type in ('PLUS', 'MINUS'):
|
|
1811
|
+
op_token = self.consume()
|
|
1812
|
+
op_val = op_token.value
|
|
1813
|
+
# Normalize to symbols
|
|
1814
|
+
if op_token.type == 'PLUS': op_val = '+'
|
|
1815
|
+
if op_token.type == 'MINUS': op_val = '-'
|
|
1816
|
+
|
|
1817
|
+
right = self.parse_term()
|
|
1818
|
+
new_node = BinOp(left, op_val, right)
|
|
1819
|
+
new_node.line = op_token.line
|
|
1820
|
+
left = new_node
|
|
1821
|
+
return left
|
|
1822
|
+
def parse_term(self) -> Node:
|
|
1823
|
+
left = self.parse_factor()
|
|
1824
|
+
while self.peek().type in ('MUL', 'DIV', 'MOD', 'TIMES'):
|
|
1825
|
+
# Disambiguate "repeat 3 TIMES" vs "3 TIMES 4"
|
|
1826
|
+
if self.peek().type == 'TIMES':
|
|
1827
|
+
next_tok = self.peek(1)
|
|
1828
|
+
if next_tok.type in ('COLON', 'NEWLINE'):
|
|
1829
|
+
break
|
|
1830
|
+
|
|
1831
|
+
op_token = self.consume()
|
|
1832
|
+
op_val = op_token.value
|
|
1833
|
+
|
|
1834
|
+
# Normalize
|
|
1835
|
+
if op_token.type == 'MUL': op_val = '*'
|
|
1836
|
+
if op_token.type == 'TIMES': op_val = '*'
|
|
1837
|
+
if op_token.type == 'DIV':
|
|
1838
|
+
op_val = '/'
|
|
1839
|
+
if self.check('BY'): self.consume('BY') # Handle "divided by"
|
|
1840
|
+
|
|
1841
|
+
if op_token.type == 'MOD': op_val = '%'
|
|
1842
|
+
|
|
1843
|
+
right = self.parse_factor()
|
|
1844
|
+
new_node = BinOp(left, op_val, right)
|
|
1845
|
+
new_node.line = op_token.line
|
|
1846
|
+
left = new_node
|
|
1847
|
+
return left
|
|
1848
|
+
def parse_convert(self) -> Convert:
|
|
1849
|
+
token = self.consume('CONVERT')
|
|
1850
|
+
expr = self.parse_factor()
|
|
1851
|
+
self.consume('TO')
|
|
1852
|
+
target_format = 'json'
|
|
1853
|
+
if self.check('JSON'):
|
|
1854
|
+
self.consume('JSON')
|
|
1855
|
+
elif self.check('ID'):
|
|
1856
|
+
target_format = self.consume('ID').value
|
|
1857
|
+
node = Convert(expr, target_format)
|
|
1858
|
+
node.line = token.line
|
|
1859
|
+
return node
|
|
1860
|
+
def parse_download(self) -> Download:
|
|
1861
|
+
token = self.consume('DOWNLOAD')
|
|
1862
|
+
url = self.parse_expression()
|
|
1863
|
+
self.consume('NEWLINE')
|
|
1864
|
+
node = Download(url)
|
|
1865
|
+
node.line = token.line
|
|
1866
|
+
return node
|
|
1867
|
+
def parse_archive(self) -> ArchiveOp:
|
|
1868
|
+
op = None
|
|
1869
|
+
token = None
|
|
1870
|
+
if self.check('COMPRESS'):
|
|
1871
|
+
token = self.consume('COMPRESS')
|
|
1872
|
+
op = 'compress'
|
|
1873
|
+
if self.check('FOLDER'): self.consume('FOLDER')
|
|
1874
|
+
else:
|
|
1875
|
+
token = self.consume('EXTRACT')
|
|
1876
|
+
op = 'extract'
|
|
1877
|
+
source = self.parse_expression()
|
|
1878
|
+
self.consume('TO')
|
|
1879
|
+
target = self.parse_expression()
|
|
1880
|
+
self.consume('NEWLINE')
|
|
1881
|
+
node = ArchiveOp(op, source, target)
|
|
1882
|
+
node.line = token.line
|
|
1883
|
+
return node
|
|
1884
|
+
def parse_csv_load(self) -> CsvOp:
|
|
1885
|
+
token = self.consume('LOAD')
|
|
1886
|
+
self.consume('CSV')
|
|
1887
|
+
path = self.parse_expression()
|
|
1888
|
+
self.consume('NEWLINE')
|
|
1889
|
+
node = CsvOp('load', None, path)
|
|
1890
|
+
node.line = token.line
|
|
1891
|
+
return node
|
|
1892
|
+
def parse_csv_save(self) -> CsvOp:
|
|
1893
|
+
token = self.consume('SAVE')
|
|
1894
|
+
data = self.parse_expression()
|
|
1895
|
+
self.consume('TO')
|
|
1896
|
+
self.consume('CSV')
|
|
1897
|
+
path = self.parse_expression()
|
|
1898
|
+
self.consume('NEWLINE')
|
|
1899
|
+
node = CsvOp('save', data, path)
|
|
1900
|
+
node.line = token.line
|
|
1901
|
+
return node
|
|
1902
|
+
def parse_clipboard(self) -> Node:
|
|
1903
|
+
if self.check('COPY'):
|
|
1904
|
+
token = self.consume('COPY')
|
|
1905
|
+
content = self.parse_expression()
|
|
1906
|
+
self.consume('TO')
|
|
1907
|
+
self.consume('CLIPBOARD')
|
|
1908
|
+
self.consume('NEWLINE')
|
|
1909
|
+
node = ClipboardOp('copy', content)
|
|
1910
|
+
node.line = token.line
|
|
1911
|
+
return node
|
|
1912
|
+
else:
|
|
1913
|
+
token = self.consume('PASTE')
|
|
1914
|
+
self.consume('FROM')
|
|
1915
|
+
self.consume('CLIPBOARD')
|
|
1916
|
+
self.consume('NEWLINE')
|
|
1917
|
+
node = ClipboardOp('paste', None)
|
|
1918
|
+
node.line = token.line
|
|
1919
|
+
return node
|
|
1920
|
+
def parse_automation(self) -> AutomationOp:
|
|
1921
|
+
if self.check('PRESS'):
|
|
1922
|
+
token = self.consume('PRESS')
|
|
1923
|
+
keys = self.parse_expression()
|
|
1924
|
+
self.consume('NEWLINE')
|
|
1925
|
+
return AutomationOp('press', [keys])
|
|
1926
|
+
elif self.check('TYPE'):
|
|
1927
|
+
token = self.consume('TYPE')
|
|
1928
|
+
text = self.parse_expression()
|
|
1929
|
+
self.consume('NEWLINE')
|
|
1930
|
+
return AutomationOp('type', [text])
|
|
1931
|
+
elif self.check('CLICK'):
|
|
1932
|
+
token = self.consume('CLICK')
|
|
1933
|
+
self.consume('AT')
|
|
1934
|
+
x = self.parse_expression()
|
|
1935
|
+
if self.check('COMMA'): self.consume('COMMA')
|
|
1936
|
+
y = self.parse_expression()
|
|
1937
|
+
self.consume('NEWLINE')
|
|
1938
|
+
return AutomationOp('click', [x, y])
|
|
1939
|
+
elif self.check('NOTIFY'):
|
|
1940
|
+
token = self.consume('NOTIFY')
|
|
1941
|
+
title = self.parse_expression()
|
|
1942
|
+
msg = self.parse_expression()
|
|
1943
|
+
self.consume('NEWLINE')
|
|
1944
|
+
return AutomationOp('notify', [title, msg])
|
|
1945
|
+
def parse_write(self) -> FileWrite:
|
|
1946
|
+
token = self.consume('WRITE')
|
|
1947
|
+
content = self.parse_expression()
|
|
1948
|
+
self.consume('TO')
|
|
1949
|
+
self.consume('FILE')
|
|
1950
|
+
path = self.parse_expression()
|
|
1951
|
+
self.consume('NEWLINE')
|
|
1952
|
+
node = FileWrite(path, content, 'w')
|
|
1953
|
+
node.line = token.line
|
|
1954
|
+
return node
|
|
1955
|
+
def parse_append(self) -> FileWrite:
|
|
1956
|
+
token = self.consume('APPEND')
|
|
1957
|
+
content = self.parse_expression()
|
|
1958
|
+
self.consume('TO')
|
|
1959
|
+
self.consume('FILE')
|
|
1960
|
+
path = self.parse_expression()
|
|
1961
|
+
self.consume('NEWLINE')
|
|
1962
|
+
node = FileWrite(path, content, 'a')
|
|
1963
|
+
node.line = token.line
|
|
1964
|
+
return node
|
|
1965
|
+
|
|
1966
|
+
|
|
1967
|
+
def parse_increment(self) -> Assign:
|
|
1968
|
+
|
|
1969
|
+
token = self.consume('INCREMENT')
|
|
1970
|
+
|
|
1971
|
+
name = self.consume('ID').value
|
|
1972
|
+
|
|
1973
|
+
amount = Number(1)
|
|
1974
|
+
|
|
1975
|
+
if self.check('BY'):
|
|
1976
|
+
|
|
1977
|
+
self.consume('BY')
|
|
1978
|
+
|
|
1979
|
+
amount = self.parse_expression()
|
|
1980
|
+
|
|
1981
|
+
self.consume('NEWLINE')
|
|
1982
|
+
|
|
1983
|
+
node = Assign(name, BinOp(VarAccess(name), '+', amount))
|
|
1984
|
+
|
|
1985
|
+
node.line = token.line
|
|
1986
|
+
|
|
1987
|
+
return node
|
|
1988
|
+
|
|
1989
|
+
|
|
1990
|
+
|
|
1991
|
+
def parse_decrement(self) -> Assign:
|
|
1992
|
+
|
|
1993
|
+
token = self.consume('DECREMENT')
|
|
1994
|
+
|
|
1995
|
+
name = self.consume('ID').value
|
|
1996
|
+
|
|
1997
|
+
amount = Number(1)
|
|
1998
|
+
|
|
1999
|
+
if self.check('BY'):
|
|
2000
|
+
|
|
2001
|
+
self.consume('BY')
|
|
2002
|
+
|
|
2003
|
+
amount = self.parse_expression()
|
|
2004
|
+
|
|
2005
|
+
self.consume('NEWLINE')
|
|
2006
|
+
|
|
2007
|
+
node = Assign(name, BinOp(VarAccess(name), '-', amount))
|
|
2008
|
+
|
|
2009
|
+
node.line = token.line
|
|
2010
|
+
|
|
2011
|
+
return node
|
|
2012
|
+
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
def parse_multiply(self) -> Assign:
|
|
2016
|
+
|
|
2017
|
+
token = self.consume('MULTIPLY')
|
|
2018
|
+
|
|
2019
|
+
name = self.consume('ID').value
|
|
2020
|
+
|
|
2021
|
+
self.consume('BY')
|
|
2022
|
+
|
|
2023
|
+
amount = self.parse_expression()
|
|
2024
|
+
|
|
2025
|
+
self.consume('NEWLINE')
|
|
2026
|
+
|
|
2027
|
+
node = Assign(name, BinOp(VarAccess(name), '*', amount))
|
|
2028
|
+
|
|
2029
|
+
node.line = token.line
|
|
2030
|
+
|
|
2031
|
+
return node
|
|
2032
|
+
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
def parse_divide(self) -> Assign:
|
|
2036
|
+
|
|
2037
|
+
token = self.consume('DIVIDE')
|
|
2038
|
+
|
|
2039
|
+
name = self.consume('ID').value
|
|
2040
|
+
|
|
2041
|
+
self.consume('BY')
|
|
2042
|
+
|
|
2043
|
+
amount = self.parse_expression()
|
|
2044
|
+
|
|
2045
|
+
self.consume('NEWLINE')
|
|
2046
|
+
|
|
2047
|
+
node = Assign(name, BinOp(VarAccess(name), '/', amount))
|
|
2048
|
+
|
|
2049
|
+
node.line = token.line
|
|
2050
|
+
|
|
2051
|
+
return node
|
|
2052
|
+
|
|
2053
|
+
|
|
2054
|
+
|
|
2055
|
+
def parse_set(self) -> Assign:
|
|
2056
|
+
|
|
2057
|
+
token = self.consume('SET')
|
|
2058
|
+
|
|
2059
|
+
name = self.consume('ID').value
|
|
2060
|
+
|
|
2061
|
+
self.consume('TO')
|
|
2062
|
+
|
|
2063
|
+
value = self.parse_expression()
|
|
2064
|
+
|
|
2065
|
+
self.consume('NEWLINE')
|
|
2066
|
+
|
|
2067
|
+
node = Assign(name, value)
|
|
2068
|
+
|
|
2069
|
+
node.line = token.line
|
|
2070
|
+
|
|
2071
|
+
return node
|
|
2072
|
+
|
|
2073
|
+
|
|
2074
|
+
|
|
2075
|
+
def parse_sum(self) -> Node:
|
|
2076
|
+
token = self.consume('SUM')
|
|
2077
|
+
self.consume('OF')
|
|
2078
|
+
|
|
2079
|
+
# Check for 'numbers from ...' (contextual keyword 'numbers')
|
|
2080
|
+
if self.check('ID') and self.peek().value == 'numbers':
|
|
2081
|
+
range_node = self.parse_numbers_range()
|
|
2082
|
+
# range_node is Call('range_list', ...)
|
|
2083
|
+
# We want Call('sum', [range_node])
|
|
2084
|
+
node = Call('sum', [range_node])
|
|
2085
|
+
node.line = token.line
|
|
2086
|
+
return node
|
|
2087
|
+
|
|
2088
|
+
expr = self.parse_expression()
|
|
2089
|
+
node = Call('sum', [expr])
|
|
2090
|
+
node.line = token.line
|
|
2091
|
+
return node
|
|
2092
|
+
|
|
2093
|
+
|
|
2094
|
+
|
|
2095
|
+
def parse_upper(self) -> Node:
|
|
2096
|
+
token = self.consume('UPPER')
|
|
2097
|
+
expr = self.parse_expression()
|
|
2098
|
+
only_letters = Boolean(False)
|
|
2099
|
+
|
|
2100
|
+
if self.check('ID') and self.peek().value == 'only':
|
|
2101
|
+
self.consume() # consume 'only'
|
|
2102
|
+
if self.check('ID') and self.peek().value == 'letters':
|
|
2103
|
+
self.consume() # consume 'letters'
|
|
2104
|
+
only_letters = Boolean(True)
|
|
2105
|
+
node = Call('upper', [expr, only_letters])
|
|
2106
|
+
node.line = token.line
|
|
2107
|
+
return node
|
|
2108
|
+
|
|
2109
|
+
|
|
2110
|
+
|
|
2111
|
+
def parse_numbers_range(self) -> Node:
|
|
2112
|
+
# Expect 'numbers' as ID
|
|
2113
|
+
token = self.peek()
|
|
2114
|
+
if self.check('ID') and self.peek().value == 'numbers':
|
|
2115
|
+
self.consume()
|
|
2116
|
+
else:
|
|
2117
|
+
# Should be 'numbers' but if called from parse_sum we assume check passed.
|
|
2118
|
+
# If called from Factor loop...
|
|
2119
|
+
pass
|
|
2120
|
+
|
|
2121
|
+
self.consume('FROM')
|
|
2122
|
+
start = self.parse_expression()
|
|
2123
|
+
self.consume('TO')
|
|
2124
|
+
end = self.parse_expression()
|
|
2125
|
+
|
|
2126
|
+
condition = None
|
|
2127
|
+
if self.check('ID') and self.peek().value == 'that':
|
|
2128
|
+
self.consume() # that
|
|
2129
|
+
if self.check('ID') and self.peek().value == 'are':
|
|
2130
|
+
self.consume() # are
|
|
2131
|
+
|
|
2132
|
+
if self.check('ID') and self.peek().value == 'prime':
|
|
2133
|
+
self.consume() # prime
|
|
2134
|
+
condition = String('prime')
|
|
2135
|
+
elif self.check('ID') and self.peek().value == 'digits':
|
|
2136
|
+
self.consume() # digits
|
|
2137
|
+
condition = String('digits')
|
|
2138
|
+
|
|
2139
|
+
elif self.check('WHEN'):
|
|
2140
|
+
self.consume('WHEN')
|
|
2141
|
+
# 'when even' -> check for ID 'even' or expression?
|
|
2142
|
+
# User example: 'when even'. Implicit variable?
|
|
2143
|
+
# Let's verify repro: 'when even'
|
|
2144
|
+
if self.check('ID') and self.peek().value == 'even':
|
|
2145
|
+
self.consume()
|
|
2146
|
+
condition = String('even')
|
|
2147
|
+
elif self.check('ID') and self.peek().value == 'odd':
|
|
2148
|
+
self.consume()
|
|
2149
|
+
condition = String('odd')
|
|
2150
|
+
else:
|
|
2151
|
+
# TODO: handle generic expression filter if needed
|
|
2152
|
+
pass
|
|
2153
|
+
|
|
2154
|
+
node = Call('range_list', [start, end, condition if condition else Boolean(False)])
|
|
2155
|
+
node.line = token.line
|
|
2156
|
+
return node
|
|
2157
|
+
def parse_add_to_list(self) -> Node:
|
|
2158
|
+
token = self.consume('ADD')
|
|
2159
|
+
item = self.parse_expression()
|
|
2160
|
+
self.consume('TO')
|
|
2161
|
+
list_expr = self.parse_expression()
|
|
2162
|
+
self.consume('NEWLINE')
|
|
2163
|
+
node = Call('append', [list_expr, item])
|
|
2164
|
+
node.line = token.line
|
|
2165
|
+
return node
|
|
2166
|
+
|
|
2167
|
+
def parse_remove_from_list(self) -> Node:
|
|
2168
|
+
token = self.consume('REMOVE')
|
|
2169
|
+
item = self.parse_expression()
|
|
2170
|
+
self.consume('FROM')
|
|
2171
|
+
list_expr = self.parse_expression()
|
|
2172
|
+
self.consume('NEWLINE')
|
|
2173
|
+
node = Call('remove', [list_expr, item])
|
|
2174
|
+
node.line = token.line
|
|
2175
|
+
return node
|
|
2176
|
+
|
|
2177
|
+
def parse_wait(self) -> Node:
|
|
2178
|
+
token = self.consume('WAIT')
|
|
2179
|
+
value = self.parse_expression()
|
|
2180
|
+
if self.check('SECOND'): self.consume('SECOND')
|
|
2181
|
+
elif self.check('SECONDS'): self.consume('SECONDS') # Assuming 'SECONDS' token maps to SECOND?
|
|
2182
|
+
# Actually I need to check lexer mapping for MINUTES/SECONDS.
|
|
2183
|
+
# Lexer has: 'minute': 'MINUTE', 'minutes': 'MINUTE', 'second': 'SECOND', 'seconds': 'SECOND'
|
|
2184
|
+
|
|
2185
|
+
elif self.check('MINUTE'):
|
|
2186
|
+
self.consume('MINUTE')
|
|
2187
|
+
value = BinOp(value, '*', Number(60))
|
|
2188
|
+
|
|
2189
|
+
self.consume('NEWLINE')
|
|
2190
|
+
node = Call('wait', [value])
|
|
2191
|
+
node.line = token.line
|
|
2192
|
+
return node
|
|
2193
|
+
|
|
2194
|
+
def parse_add_distinguish(self) -> Node:
|
|
2195
|
+
# Distinguish "ADD <expr> TO <list>" vs "ADD <component> ..."
|
|
2196
|
+
tok = self.peek(1)
|
|
2197
|
+
if tok.type in ('BUTTON', 'HEADING', 'PARAGRAPH', 'IMAGE', 'APP', 'PAGE', 'Use', 'INPUT', 'TEXT'):
|
|
2198
|
+
return self.parse_add_to()
|
|
2199
|
+
else:
|
|
2200
|
+
return self.parse_add_to_list()
|
|
2201
|
+
|
|
2202
|
+
def parse_make_assignment(self) -> Node:
|
|
2203
|
+
token = self.consume('MAKE')
|
|
2204
|
+
name = self.consume('ID').value
|
|
2205
|
+
if self.check('BE'): self.consume('BE')
|
|
2206
|
+
value = self.parse_expression()
|
|
2207
|
+
self.consume('NEWLINE')
|
|
2208
|
+
node = Assign(name, value)
|
|
2209
|
+
node.line = token.line
|
|
2210
|
+
return node
|
|
2211
|
+
|
|
2212
|
+
def parse_as_long_as(self) -> While:
|
|
2213
|
+
start_token = self.consume('AS')
|
|
2214
|
+
self.consume('LONG')
|
|
2215
|
+
self.consume('AS')
|
|
2216
|
+
condition = self.parse_expression()
|
|
2217
|
+
if self.check('COLON'): self.consume('COLON')
|
|
2218
|
+
self.consume('NEWLINE')
|
|
2219
|
+
self.consume('INDENT')
|
|
2220
|
+
body = []
|
|
2221
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
2222
|
+
while self.check('NEWLINE'): self.consume()
|
|
2223
|
+
if self.check('DEDENT'): break
|
|
2224
|
+
body.append(self.parse_statement())
|
|
2225
|
+
self.consume('DEDENT')
|
|
2226
|
+
node = While(condition, body)
|
|
2227
|
+
node.line = start_token.line
|
|
2228
|
+
return node
|
|
2229
|
+
|