shell-lite 0.3.3__py3-none-any.whl → 0.3.5__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/__init__.py +1 -0
- shell_lite/ast_nodes.py +15 -110
- shell_lite/cli.py +10 -0
- shell_lite/compiler.py +2 -189
- shell_lite/formatter.py +75 -0
- shell_lite/interpreter.py +35 -538
- shell_lite/js_compiler.py +3 -79
- shell_lite/lexer.py +29 -107
- shell_lite/main.py +120 -75
- shell_lite/parser.py +17 -510
- shell_lite/runtime.py +1 -76
- shell_lite-0.3.5.dist-info/LICENSE +21 -0
- shell_lite-0.3.5.dist-info/METADATA +40 -0
- shell_lite-0.3.5.dist-info/RECORD +17 -0
- {shell_lite-0.3.3.dist-info → shell_lite-0.3.5.dist-info}/WHEEL +1 -1
- shell_lite-0.3.3.dist-info/METADATA +0 -77
- shell_lite-0.3.3.dist-info/RECORD +0 -14
- {shell_lite-0.3.3.dist-info → shell_lite-0.3.5.dist-info}/entry_points.txt +0 -0
- {shell_lite-0.3.3.dist-info → shell_lite-0.3.5.dist-info}/top_level.txt +0 -0
shell_lite/parser.py
CHANGED
|
@@ -2,43 +2,33 @@ from typing import List, Optional
|
|
|
2
2
|
from .lexer import Token, Lexer
|
|
3
3
|
from .ast_nodes import *
|
|
4
4
|
import re
|
|
5
|
-
|
|
6
5
|
class Parser:
|
|
7
6
|
def __init__(self, tokens: List[Token]):
|
|
8
|
-
self.tokens = tokens
|
|
7
|
+
self.tokens = [t for t in tokens if t.type != 'COMMENT']
|
|
9
8
|
self.pos = 0
|
|
10
|
-
|
|
11
9
|
def peek(self, offset: int = 0) -> Token:
|
|
12
10
|
if self.pos + offset < len(self.tokens):
|
|
13
11
|
return self.tokens[self.pos + offset]
|
|
14
12
|
return self.tokens[-1]
|
|
15
|
-
|
|
16
13
|
def consume(self, expected_type: str = None) -> Token:
|
|
17
14
|
token = self.peek()
|
|
18
15
|
if expected_type and token.type != expected_type:
|
|
19
16
|
raise SyntaxError(f"Expected {expected_type} but got {token.type} on line {token.line}")
|
|
20
17
|
self.pos += 1
|
|
21
18
|
return token
|
|
22
|
-
|
|
23
19
|
def check(self, token_type: str) -> bool:
|
|
24
20
|
return self.peek().type == token_type
|
|
25
|
-
|
|
26
21
|
def parse(self) -> List[Node]:
|
|
27
22
|
statements = []
|
|
28
23
|
while not self.check('EOF'):
|
|
29
|
-
# Skip newlines at the top level between statements
|
|
30
24
|
while self.check('NEWLINE'):
|
|
31
25
|
self.consume()
|
|
32
26
|
if self.check('EOF'): break
|
|
33
|
-
|
|
34
27
|
if self.check('EOF'): break
|
|
35
|
-
|
|
36
28
|
stmt = self.parse_statement()
|
|
37
29
|
if stmt:
|
|
38
30
|
statements.append(stmt)
|
|
39
31
|
return statements
|
|
40
|
-
|
|
41
|
-
|
|
42
32
|
def parse_statement(self) -> Node:
|
|
43
33
|
if self.check('USE'):
|
|
44
34
|
return self.parse_import()
|
|
@@ -87,38 +77,23 @@ class Parser:
|
|
|
87
77
|
elif self.check('MAKE'):
|
|
88
78
|
return self.parse_make()
|
|
89
79
|
elif self.check('INPUT'):
|
|
90
|
-
# Check if this is 'input type="..."' i.e. HTML tag
|
|
91
80
|
next_t = self.peek(1)
|
|
92
81
|
if next_t.type in ('ID', 'TYPE', 'STRING', 'NAME', 'VALUE', 'CLASS', 'STYLE', 'ONCLICK', 'SRC', 'HREF', 'ACTION', 'METHOD'):
|
|
93
|
-
# Treat as HTML tag function call - use parse_id_start_statement with token passed
|
|
94
82
|
input_token = self.consume()
|
|
95
83
|
return self.parse_id_start_statement(passed_name_token=input_token)
|
|
96
|
-
# Else fallthrough to expression
|
|
97
84
|
return self.parse_expression_stmt()
|
|
98
85
|
elif self.check('ID'):
|
|
99
86
|
return self.parse_id_start_statement()
|
|
100
87
|
elif self.check('SPAWN'):
|
|
101
|
-
# Support spawn as a statement (ignore return value)
|
|
102
|
-
# e.g. spawn my_func()
|
|
103
88
|
expr = self.parse_expression()
|
|
104
89
|
self.consume('NEWLINE')
|
|
105
|
-
return Print(expr)
|
|
106
|
-
# Actually parse_expression_stmt handles this if we fall through
|
|
107
|
-
# But here we caught check('SPAWN'), so we must handle it or let fallthrough?
|
|
108
|
-
# check('ID') is above. SPAWN is a keyword.
|
|
109
|
-
# So we must handle it explicitly or remove this block and let 'else' handle parse_expression_stmt.
|
|
110
|
-
# BUT parse_expression_stmt calls parse_expression.
|
|
111
|
-
# Does parse_expression handle SPAWN at top level?
|
|
112
|
-
# Yes if added to parse_factor/parse_expression.
|
|
113
|
-
# So we can remove this block if SPAWN starts an expression.
|
|
90
|
+
return Print(expr)
|
|
114
91
|
pass
|
|
115
92
|
elif self.check('WAIT'):
|
|
116
93
|
return self.parse_wait()
|
|
117
94
|
elif self.check('EVERY'):
|
|
118
95
|
return self.parse_every()
|
|
119
96
|
elif self.check('IN'):
|
|
120
|
-
# "In 10 minutes" - Check if it looks like a time duration start
|
|
121
|
-
# If just 'IN' and we are at statement level, likely 'After'
|
|
122
97
|
return self.parse_after()
|
|
123
98
|
elif self.check('LISTEN'):
|
|
124
99
|
return self.parse_listen()
|
|
@@ -129,12 +104,11 @@ class Parser:
|
|
|
129
104
|
elif self.check('COMPRESS') or self.check('EXTRACT'):
|
|
130
105
|
return self.parse_archive()
|
|
131
106
|
elif self.check('LOAD') or self.check('SAVE'):
|
|
132
|
-
# Check if it's CSV? "load csv" or "save x to csv"
|
|
133
107
|
if self.check('LOAD') and self.peek(1).type == 'CSV':
|
|
134
108
|
return self.parse_csv_load()
|
|
135
109
|
if self.check('SAVE'):
|
|
136
|
-
return self.parse_csv_save()
|
|
137
|
-
return self.parse_expression_stmt()
|
|
110
|
+
return self.parse_csv_save()
|
|
111
|
+
return self.parse_expression_stmt()
|
|
138
112
|
elif self.check('COPY') or self.check('PASTE'):
|
|
139
113
|
return self.parse_clipboard()
|
|
140
114
|
elif self.check('WRITE'):
|
|
@@ -147,7 +121,6 @@ class Parser:
|
|
|
147
121
|
return self.parse_automation()
|
|
148
122
|
elif self.check('BEFORE'):
|
|
149
123
|
return self.parse_middleware()
|
|
150
|
-
# === NATURAL ENGLISH WEB DSL ===
|
|
151
124
|
elif self.check('DEFINE'):
|
|
152
125
|
return self.parse_define_page()
|
|
153
126
|
elif self.check('ADD'):
|
|
@@ -160,7 +133,6 @@ class Parser:
|
|
|
160
133
|
return self.parse_paragraph()
|
|
161
134
|
else:
|
|
162
135
|
return self.parse_expression_stmt()
|
|
163
|
-
|
|
164
136
|
def parse_alert(self) -> Alert:
|
|
165
137
|
token = self.consume('ALERT')
|
|
166
138
|
message = self.parse_expression()
|
|
@@ -168,10 +140,6 @@ class Parser:
|
|
|
168
140
|
node = Alert(message)
|
|
169
141
|
node.line = token.line
|
|
170
142
|
return node
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
143
|
def parse_const(self) -> ConstAssign:
|
|
176
144
|
token = self.consume('CONST')
|
|
177
145
|
name = self.consume('ID').value
|
|
@@ -181,54 +149,38 @@ class Parser:
|
|
|
181
149
|
node = ConstAssign(name, value)
|
|
182
150
|
node.line = token.line
|
|
183
151
|
return node
|
|
184
|
-
|
|
185
|
-
|
|
186
152
|
def parse_on(self) -> Node:
|
|
187
|
-
"""Parse: on file_change "path" ... OR on request to "/path" ..."""
|
|
188
153
|
token = self.consume('ON')
|
|
189
|
-
|
|
190
154
|
if self.check('REQUEST') or (self.check('ID') and self.peek().value == 'request'):
|
|
191
|
-
# on request to "/path"
|
|
192
155
|
self.consume()
|
|
193
156
|
if self.check('TO'): self.consume('TO')
|
|
194
|
-
|
|
195
|
-
path = self.parse_expression() # path pattern
|
|
196
|
-
|
|
157
|
+
path = self.parse_expression()
|
|
197
158
|
self.consume('NEWLINE')
|
|
198
159
|
self.consume('INDENT')
|
|
199
|
-
|
|
200
160
|
body = []
|
|
201
161
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
202
162
|
while self.check('NEWLINE'): self.consume()
|
|
203
163
|
if self.check('DEDENT'): break
|
|
204
164
|
body.append(self.parse_statement())
|
|
205
165
|
self.consume('DEDENT')
|
|
206
|
-
|
|
207
166
|
node = OnRequest(path, body)
|
|
208
167
|
node.line = token.line
|
|
209
168
|
return node
|
|
210
|
-
|
|
211
169
|
event_type = self.consume('ID').value
|
|
212
170
|
path = self.parse_expression()
|
|
213
|
-
|
|
214
171
|
self.consume('NEWLINE')
|
|
215
172
|
self.consume('INDENT')
|
|
216
|
-
|
|
217
173
|
body = []
|
|
218
174
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
219
175
|
while self.check('NEWLINE'): self.consume()
|
|
220
176
|
if self.check('DEDENT'): break
|
|
221
177
|
body.append(self.parse_statement())
|
|
222
|
-
|
|
223
178
|
self.consume('DEDENT')
|
|
224
179
|
return FileWatcher(path, body)
|
|
225
|
-
|
|
226
180
|
def _parse_natural_list(self) -> ListVal:
|
|
227
|
-
|
|
228
|
-
self.consume('ID') # a
|
|
181
|
+
self.consume('ID')
|
|
229
182
|
self.consume('LIST')
|
|
230
183
|
self.consume('OF')
|
|
231
|
-
|
|
232
184
|
elements = []
|
|
233
185
|
if not self.check('NEWLINE') and not self.check('EOF'):
|
|
234
186
|
elements.append(self.parse_expression())
|
|
@@ -236,17 +188,13 @@ class Parser:
|
|
|
236
188
|
self.consume('COMMA')
|
|
237
189
|
if self.check('NEWLINE'): break
|
|
238
190
|
elements.append(self.parse_expression())
|
|
239
|
-
|
|
240
191
|
node = ListVal(elements)
|
|
241
192
|
return node
|
|
242
|
-
|
|
243
193
|
def _parse_natural_set(self) -> Node:
|
|
244
|
-
|
|
245
|
-
self.consume('ID') # a
|
|
194
|
+
self.consume('ID')
|
|
246
195
|
self.consume('UNIQUE')
|
|
247
196
|
self.consume('SET')
|
|
248
197
|
self.consume('OF')
|
|
249
|
-
|
|
250
198
|
elements = []
|
|
251
199
|
if not self.check('NEWLINE') and not self.check('EOF'):
|
|
252
200
|
elements.append(self.parse_expression())
|
|
@@ -254,82 +202,54 @@ class Parser:
|
|
|
254
202
|
self.consume('COMMA')
|
|
255
203
|
if self.check('NEWLINE'): break
|
|
256
204
|
elements.append(self.parse_expression())
|
|
257
|
-
|
|
258
205
|
list_node = ListVal(elements)
|
|
259
206
|
return Call('Set', [list_node])
|
|
260
|
-
|
|
261
207
|
def parse_wait(self) -> Node:
|
|
262
|
-
"""Parse: wait for 2 seconds (or just wait 2)"""
|
|
263
208
|
token = self.consume('WAIT')
|
|
264
|
-
|
|
265
|
-
# Optional 'for'
|
|
266
|
-
# 'for' is a keyword FOR
|
|
267
209
|
if self.check('FOR'):
|
|
268
210
|
self.consume('FOR')
|
|
269
|
-
|
|
270
211
|
time_expr = self.parse_expression()
|
|
271
|
-
|
|
272
|
-
# Optional 'seconds' or 'second' filler
|
|
273
212
|
if self.check('SECOND'):
|
|
274
213
|
self.consume()
|
|
275
|
-
|
|
276
214
|
self.consume('NEWLINE')
|
|
277
|
-
|
|
278
|
-
# Return as a Call to 'wait' builtin
|
|
279
|
-
# Standard Call node: Call(func_name, args)
|
|
280
215
|
return Call('wait', [time_expr])
|
|
281
|
-
|
|
282
|
-
# --- New English-like statement parsers ---
|
|
283
|
-
|
|
284
216
|
def parse_stop(self) -> Stop:
|
|
285
|
-
"""Parse: stop"""
|
|
286
217
|
token = self.consume('STOP')
|
|
287
218
|
self.consume('NEWLINE')
|
|
288
219
|
node = Stop()
|
|
289
220
|
node.line = token.line
|
|
290
221
|
return node
|
|
291
|
-
|
|
292
222
|
def parse_skip(self) -> Skip:
|
|
293
|
-
"""Parse: skip"""
|
|
294
223
|
token = self.consume('SKIP')
|
|
295
224
|
self.consume('NEWLINE')
|
|
296
225
|
node = Skip()
|
|
297
226
|
node.line = token.line
|
|
298
227
|
return node
|
|
299
|
-
|
|
300
228
|
def parse_error(self) -> Throw:
|
|
301
|
-
"""Parse: error 'message'"""
|
|
302
229
|
token = self.consume('ERROR')
|
|
303
230
|
message = self.parse_expression()
|
|
304
231
|
self.consume('NEWLINE')
|
|
305
232
|
node = Throw(message)
|
|
306
233
|
node.line = token.line
|
|
307
234
|
return node
|
|
308
|
-
|
|
309
235
|
def parse_execute(self) -> Execute:
|
|
310
|
-
"""Parse: execute 'code string'"""
|
|
311
236
|
token = self.consume('EXECUTE')
|
|
312
237
|
code = self.parse_expression()
|
|
313
238
|
self.consume('NEWLINE')
|
|
314
239
|
node = Execute(code)
|
|
315
240
|
node.line = token.line
|
|
316
241
|
return node
|
|
317
|
-
|
|
318
242
|
def parse_unless(self) -> Unless:
|
|
319
|
-
"""Parse: unless condition (body)"""
|
|
320
243
|
token = self.consume('UNLESS')
|
|
321
244
|
condition = self.parse_expression()
|
|
322
245
|
self.consume('NEWLINE')
|
|
323
246
|
self.consume('INDENT')
|
|
324
|
-
|
|
325
247
|
body = []
|
|
326
248
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
327
249
|
while self.check('NEWLINE'): self.consume()
|
|
328
250
|
if self.check('DEDENT'): break
|
|
329
251
|
body.append(self.parse_statement())
|
|
330
|
-
|
|
331
252
|
self.consume('DEDENT')
|
|
332
|
-
|
|
333
253
|
else_body = None
|
|
334
254
|
if self.check('ELSE'):
|
|
335
255
|
self.consume('ELSE')
|
|
@@ -341,48 +261,37 @@ class Parser:
|
|
|
341
261
|
if self.check('DEDENT'): break
|
|
342
262
|
else_body.append(self.parse_statement())
|
|
343
263
|
self.consume('DEDENT')
|
|
344
|
-
|
|
345
264
|
node = Unless(condition, body, else_body)
|
|
346
265
|
node.line = token.line
|
|
347
266
|
return node
|
|
348
|
-
|
|
349
267
|
def parse_until(self) -> Until:
|
|
350
|
-
"""Parse: until condition (body)"""
|
|
351
268
|
token = self.consume('UNTIL')
|
|
352
269
|
condition = self.parse_expression()
|
|
353
270
|
self.consume('NEWLINE')
|
|
354
271
|
self.consume('INDENT')
|
|
355
|
-
|
|
356
272
|
body = []
|
|
357
273
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
358
274
|
while self.check('NEWLINE'): self.consume()
|
|
359
275
|
if self.check('DEDENT'): break
|
|
360
276
|
body.append(self.parse_statement())
|
|
361
|
-
|
|
362
277
|
self.consume('DEDENT')
|
|
363
278
|
node = Until(condition, body)
|
|
364
279
|
node.line = token.line
|
|
365
280
|
return node
|
|
366
|
-
|
|
367
281
|
def parse_forever(self) -> Forever:
|
|
368
|
-
"""Parse: forever (body) - infinite loop"""
|
|
369
282
|
token = self.consume('FOREVER')
|
|
370
283
|
self.consume('NEWLINE')
|
|
371
284
|
self.consume('INDENT')
|
|
372
|
-
|
|
373
285
|
body = []
|
|
374
286
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
375
287
|
while self.check('NEWLINE'): self.consume()
|
|
376
288
|
if self.check('DEDENT'): break
|
|
377
289
|
body.append(self.parse_statement())
|
|
378
|
-
|
|
379
290
|
self.consume('DEDENT')
|
|
380
291
|
node = Forever(body)
|
|
381
292
|
node.line = token.line
|
|
382
293
|
return node
|
|
383
|
-
|
|
384
294
|
def parse_exit(self) -> Exit:
|
|
385
|
-
"""Parse: exit or exit 1"""
|
|
386
295
|
token = self.consume('EXIT')
|
|
387
296
|
code = None
|
|
388
297
|
if not self.check('NEWLINE'):
|
|
@@ -391,108 +300,69 @@ class Parser:
|
|
|
391
300
|
node = Exit(code)
|
|
392
301
|
node.line = token.line
|
|
393
302
|
return node
|
|
394
|
-
|
|
395
303
|
def parse_make(self) -> Node:
|
|
396
|
-
"""Parse: make Robot 'name' 100 or new Robot 'name' 100"""
|
|
397
304
|
token = self.consume('MAKE')
|
|
398
305
|
class_name = self.consume('ID').value
|
|
399
|
-
|
|
400
306
|
args = []
|
|
401
307
|
while not self.check('NEWLINE') and not self.check('EOF'):
|
|
402
308
|
args.append(self.parse_expression())
|
|
403
|
-
|
|
404
309
|
self.consume('NEWLINE')
|
|
405
310
|
node = Make(class_name, args)
|
|
406
311
|
node.line = token.line
|
|
407
312
|
return node
|
|
408
|
-
|
|
409
313
|
def parse_repeat(self) -> Repeat:
|
|
410
|
-
"""Parse: repeat 5 times (body) or repeat (body)"""
|
|
411
314
|
token = self.consume('REPEAT')
|
|
412
|
-
|
|
413
|
-
# Check if there's a count
|
|
414
315
|
if self.check('NEWLINE'):
|
|
415
|
-
# Infinite loop style - but we'll require a count
|
|
416
316
|
raise SyntaxError(f"repeat requires a count on line {token.line}")
|
|
417
|
-
|
|
418
317
|
count = self.parse_expression()
|
|
419
|
-
|
|
420
|
-
# Optional 'times' keyword
|
|
421
318
|
if self.check('TIMES'):
|
|
422
319
|
self.consume('TIMES')
|
|
423
|
-
|
|
424
320
|
self.consume('NEWLINE')
|
|
425
321
|
self.consume('INDENT')
|
|
426
|
-
|
|
427
322
|
body = []
|
|
428
323
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
429
324
|
while self.check('NEWLINE'): self.consume()
|
|
430
325
|
if self.check('DEDENT'): break
|
|
431
326
|
body.append(self.parse_statement())
|
|
432
|
-
|
|
433
327
|
self.consume('DEDENT')
|
|
434
328
|
node = Repeat(count, body)
|
|
435
329
|
node.line = token.line
|
|
436
330
|
return node
|
|
437
|
-
|
|
438
331
|
return node
|
|
439
|
-
|
|
440
332
|
def parse_db_op(self) -> DatabaseOp:
|
|
441
|
-
"""Parse: db open "path", db query "sql", db close, db exec "sql" """
|
|
442
333
|
token = self.consume('DB')
|
|
443
|
-
|
|
444
|
-
op = 'open' # default? no
|
|
334
|
+
op = 'open'
|
|
445
335
|
if self.check('OPEN'): op = 'open'; self.consume()
|
|
446
336
|
elif self.check('QUERY'): op = 'query'; self.consume()
|
|
447
337
|
elif self.check('EXEC'): op = 'exec'; self.consume()
|
|
448
338
|
elif self.check('CLOSE'): op = 'close'; self.consume()
|
|
449
339
|
else:
|
|
450
|
-
# Maybe db "path" -> open?
|
|
451
340
|
if self.check('STRING'):
|
|
452
341
|
op = 'open'
|
|
453
342
|
else:
|
|
454
343
|
raise SyntaxError(f"Unknown db operation at line {token.line}")
|
|
455
|
-
|
|
456
344
|
args = []
|
|
457
345
|
if op != 'close' and not self.check('NEWLINE'):
|
|
458
346
|
args.append(self.parse_expression())
|
|
459
|
-
# Support multiple args? e.g. db exec "sql" [params]
|
|
460
347
|
while not self.check('NEWLINE'):
|
|
461
348
|
args.append(self.parse_expression())
|
|
462
|
-
|
|
463
|
-
# self.consume('NEWLINE') # Don't consume newline, let caller do it
|
|
464
349
|
node = DatabaseOp(op, args)
|
|
465
350
|
node.line = token.line
|
|
466
351
|
return node
|
|
467
|
-
|
|
468
352
|
def parse_middleware(self) -> Node:
|
|
469
|
-
"""Parse: before request ..."""
|
|
470
|
-
# We reuse OnRequest node? No, usually distinct.
|
|
471
|
-
# But for now, let's treat it as a special OnRequest with pattern "*"
|
|
472
|
-
# NO, user wants 'before request' syntax.
|
|
473
|
-
# Let's add Middleware AST? Or reuse OnRequest?
|
|
474
|
-
# Reusing OnRequest with path="MIDDLEWARE" might be confusing.
|
|
475
|
-
# But 'before request' is essentially 'on request' that runs first.
|
|
476
|
-
# Let's map it to OnRequest(path='__middleware__', body)
|
|
477
353
|
token = self.consume('BEFORE')
|
|
478
354
|
self.consume('REQUEST')
|
|
479
355
|
self.consume('NEWLINE')
|
|
480
356
|
self.consume('INDENT')
|
|
481
|
-
|
|
482
357
|
body = []
|
|
483
358
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
484
359
|
while self.check('NEWLINE'): self.consume()
|
|
485
360
|
if self.check('DEDENT'): break
|
|
486
361
|
body.append(self.parse_statement())
|
|
487
362
|
self.consume('DEDENT')
|
|
488
|
-
|
|
489
363
|
return OnRequest(String('__middleware__'), body)
|
|
490
|
-
|
|
491
364
|
def parse_when(self) -> Node:
|
|
492
|
-
"""Parse: when value is x => (body) ... OR when condition (body) OR when someone visits/submits"""
|
|
493
365
|
token = self.consume('WHEN')
|
|
494
|
-
|
|
495
|
-
# Check for natural routing: when someone visits/submits "path"
|
|
496
366
|
if self.check('SOMEONE'):
|
|
497
367
|
self.consume('SOMEONE')
|
|
498
368
|
if self.check('VISITS'):
|
|
@@ -525,39 +395,29 @@ class Parser:
|
|
|
525
395
|
node = OnRequest(path, body)
|
|
526
396
|
node.line = token.line
|
|
527
397
|
return node
|
|
528
|
-
|
|
529
398
|
condition_or_value = self.parse_expression()
|
|
530
399
|
self.consume('NEWLINE')
|
|
531
400
|
self.consume('INDENT')
|
|
532
|
-
|
|
533
|
-
# Check first statement in block to decide if Switch or If
|
|
534
401
|
if self.check('IS'):
|
|
535
|
-
# It's a Switch Statement
|
|
536
402
|
cases = []
|
|
537
403
|
otherwise = None
|
|
538
|
-
|
|
539
|
-
# Loop for Switch cases
|
|
540
404
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
541
405
|
if self.check('IS'):
|
|
542
406
|
self.consume('IS')
|
|
543
407
|
match_val = self.parse_expression()
|
|
544
408
|
self.consume('NEWLINE')
|
|
545
409
|
self.consume('INDENT')
|
|
546
|
-
|
|
547
410
|
case_body = []
|
|
548
411
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
549
412
|
while self.check('NEWLINE'): self.consume()
|
|
550
413
|
if self.check('DEDENT'): break
|
|
551
414
|
case_body.append(self.parse_statement())
|
|
552
415
|
self.consume('DEDENT')
|
|
553
|
-
|
|
554
416
|
cases.append((match_val, case_body))
|
|
555
|
-
|
|
556
417
|
elif self.check('OTHERWISE'):
|
|
557
418
|
self.consume('OTHERWISE')
|
|
558
419
|
self.consume('NEWLINE')
|
|
559
420
|
self.consume('INDENT')
|
|
560
|
-
|
|
561
421
|
otherwise = []
|
|
562
422
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
563
423
|
while self.check('NEWLINE'): self.consume()
|
|
@@ -568,23 +428,17 @@ class Parser:
|
|
|
568
428
|
self.consume('NEWLINE')
|
|
569
429
|
else:
|
|
570
430
|
break
|
|
571
|
-
|
|
572
431
|
self.consume('DEDENT')
|
|
573
432
|
node = When(condition_or_value, cases, otherwise)
|
|
574
433
|
node.line = token.line
|
|
575
434
|
return node
|
|
576
|
-
|
|
577
435
|
else:
|
|
578
|
-
# It's an IF statement (when condition -> body)
|
|
579
436
|
body = []
|
|
580
437
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
581
438
|
while self.check('NEWLINE'): self.consume()
|
|
582
439
|
if self.check('DEDENT'): break
|
|
583
440
|
body.append(self.parse_statement())
|
|
584
|
-
|
|
585
441
|
self.consume('DEDENT')
|
|
586
|
-
|
|
587
|
-
# Allow else/elif for 'when' too?
|
|
588
442
|
else_body = None
|
|
589
443
|
if self.check('ELSE'):
|
|
590
444
|
self.consume('ELSE')
|
|
@@ -596,11 +450,9 @@ class Parser:
|
|
|
596
450
|
if self.check('DEDENT'): break
|
|
597
451
|
else_body.append(self.parse_statement())
|
|
598
452
|
self.consume('DEDENT')
|
|
599
|
-
|
|
600
453
|
node = If(condition_or_value, body, else_body)
|
|
601
454
|
node.line = token.line
|
|
602
455
|
return node
|
|
603
|
-
|
|
604
456
|
def parse_return(self) -> Return:
|
|
605
457
|
token = self.consume('RETURN')
|
|
606
458
|
expr = self.parse_expression()
|
|
@@ -608,20 +460,14 @@ class Parser:
|
|
|
608
460
|
node = Return(expr)
|
|
609
461
|
node.line = token.line
|
|
610
462
|
return node
|
|
611
|
-
|
|
612
463
|
def parse_function_def(self) -> FunctionDef:
|
|
613
464
|
start_token = self.consume('TO')
|
|
614
465
|
name = self.consume('ID').value
|
|
615
|
-
|
|
616
466
|
args = []
|
|
617
|
-
# Syntax: to greet name OR to greet name:string
|
|
618
467
|
while self.check('ID'):
|
|
619
468
|
arg_name = self.consume('ID').value
|
|
620
469
|
type_hint = None
|
|
621
|
-
|
|
622
|
-
# Check for Type Hint or Trailing Colon
|
|
623
470
|
if self.check('COLON'):
|
|
624
|
-
# If next is newline, it's a trailing colon (end of def)
|
|
625
471
|
if self.peek(1).type == 'NEWLINE':
|
|
626
472
|
pass
|
|
627
473
|
else:
|
|
@@ -633,46 +479,36 @@ class Parser:
|
|
|
633
479
|
self.consume()
|
|
634
480
|
else:
|
|
635
481
|
type_hint = self.consume().value
|
|
636
|
-
|
|
637
482
|
default_val = None
|
|
638
483
|
if self.check('ASSIGN'):
|
|
639
484
|
self.consume('ASSIGN')
|
|
640
485
|
default_val = self.parse_expression()
|
|
641
486
|
args.append((arg_name, default_val, type_hint))
|
|
642
|
-
|
|
643
487
|
if self.check('COLON'):
|
|
644
488
|
self.consume('COLON')
|
|
645
|
-
|
|
646
489
|
self.consume('NEWLINE')
|
|
647
490
|
self.consume('INDENT')
|
|
648
|
-
|
|
649
491
|
body = []
|
|
650
492
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
651
493
|
while self.check('NEWLINE'): self.consume()
|
|
652
494
|
if self.check('DEDENT'): break
|
|
653
495
|
body.append(self.parse_statement())
|
|
654
|
-
|
|
655
496
|
self.consume('DEDENT')
|
|
656
497
|
node = FunctionDef(name, args, body)
|
|
657
498
|
node.line = start_token.line
|
|
658
499
|
return node
|
|
659
|
-
|
|
660
500
|
def parse_class_def(self) -> ClassDef:
|
|
661
501
|
start_token = self.consume('STRUCTURE')
|
|
662
502
|
name = self.consume('ID').value
|
|
663
|
-
|
|
664
503
|
parent = None
|
|
665
504
|
if self.check('LPAREN'):
|
|
666
505
|
self.consume('LPAREN')
|
|
667
506
|
parent = self.consume('ID').value
|
|
668
507
|
self.consume('RPAREN')
|
|
669
|
-
|
|
670
508
|
self.consume('NEWLINE')
|
|
671
509
|
self.consume('INDENT')
|
|
672
|
-
|
|
673
510
|
properties = []
|
|
674
511
|
methods = []
|
|
675
|
-
|
|
676
512
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
677
513
|
if self.check('HAS'):
|
|
678
514
|
self.consume()
|
|
@@ -683,29 +519,18 @@ class Parser:
|
|
|
683
519
|
elif self.check('NEWLINE'):
|
|
684
520
|
self.consume()
|
|
685
521
|
else:
|
|
686
|
-
self.consume('DEDENT')
|
|
522
|
+
self.consume('DEDENT')
|
|
687
523
|
break
|
|
688
|
-
|
|
689
524
|
self.consume('DEDENT')
|
|
690
525
|
node = ClassDef(name, properties, methods, parent)
|
|
691
526
|
node.line = start_token.line
|
|
692
527
|
return node
|
|
693
|
-
|
|
694
528
|
def parse_id_start_statement(self, passed_name_token=None) -> Node:
|
|
695
|
-
"""
|
|
696
|
-
Handles statements starting with ID.
|
|
697
|
-
1. Assignment: name = expr
|
|
698
|
-
2. Instantiation: name is Model arg1 arg2
|
|
699
|
-
3. Function Call: name arg1 arg2
|
|
700
|
-
4. Method Call: name.method
|
|
701
|
-
5. Property Access (Expression stmt): name.prop
|
|
702
|
-
"""
|
|
703
529
|
if passed_name_token:
|
|
704
530
|
name_token = passed_name_token
|
|
705
531
|
else:
|
|
706
532
|
name_token = self.consume('ID')
|
|
707
533
|
name = name_token.value
|
|
708
|
-
|
|
709
534
|
if self.check('ASSIGN'):
|
|
710
535
|
self.consume('ASSIGN')
|
|
711
536
|
value = self.parse_expression()
|
|
@@ -713,16 +538,13 @@ class Parser:
|
|
|
713
538
|
node = Assign(name, value)
|
|
714
539
|
node.line = name_token.line
|
|
715
540
|
return node
|
|
716
|
-
|
|
717
541
|
elif self.check('PLUSEQ'):
|
|
718
542
|
self.consume('PLUSEQ')
|
|
719
543
|
value = self.parse_expression()
|
|
720
544
|
self.consume('NEWLINE')
|
|
721
|
-
# Desugar a += 1 to a = a + 1
|
|
722
545
|
node = Assign(name, BinOp(VarAccess(name), '+', value))
|
|
723
546
|
node.line = name_token.line
|
|
724
547
|
return node
|
|
725
|
-
|
|
726
548
|
elif self.check('MINUSEQ'):
|
|
727
549
|
self.consume('MINUSEQ')
|
|
728
550
|
value = self.parse_expression()
|
|
@@ -730,7 +552,6 @@ class Parser:
|
|
|
730
552
|
node = Assign(name, BinOp(VarAccess(name), '-', value))
|
|
731
553
|
node.line = name_token.line
|
|
732
554
|
return node
|
|
733
|
-
|
|
734
555
|
elif self.check('MULEQ'):
|
|
735
556
|
self.consume('MULEQ')
|
|
736
557
|
value = self.parse_expression()
|
|
@@ -738,7 +559,6 @@ class Parser:
|
|
|
738
559
|
node = Assign(name, BinOp(VarAccess(name), '*', value))
|
|
739
560
|
node.line = name_token.line
|
|
740
561
|
return node
|
|
741
|
-
|
|
742
562
|
elif self.check('DIVEQ'):
|
|
743
563
|
self.consume('DIVEQ')
|
|
744
564
|
value = self.parse_expression()
|
|
@@ -746,104 +566,78 @@ class Parser:
|
|
|
746
566
|
node = Assign(name, BinOp(VarAccess(name), '/', value))
|
|
747
567
|
node.line = name_token.line
|
|
748
568
|
return node
|
|
749
|
-
|
|
750
569
|
elif self.check('IS'):
|
|
751
570
|
token_is = self.consume('IS')
|
|
752
|
-
|
|
753
|
-
# Natural English initialization: tasks is a list
|
|
754
571
|
if self.check('ID') and self.peek().value == 'a':
|
|
755
572
|
self.consume()
|
|
756
|
-
|
|
757
573
|
if self.check('LIST'):
|
|
758
574
|
self.consume('LIST')
|
|
759
575
|
self.consume('NEWLINE')
|
|
760
576
|
node = Assign(name, ListVal([]))
|
|
761
577
|
node.line = token_is.line
|
|
762
578
|
return node
|
|
763
|
-
|
|
764
579
|
if self.check('ID') and self.peek().value in ('dictionary', 'map', 'dict'):
|
|
765
580
|
self.consume()
|
|
766
581
|
self.consume('NEWLINE')
|
|
767
582
|
node = Assign(name, Dictionary([]))
|
|
768
583
|
node.line = token_is.line
|
|
769
584
|
return node
|
|
770
|
-
|
|
771
|
-
if self.check('ID') and not self.peek().value in ('{', '['): # sanity check or just check ID
|
|
585
|
+
if self.check('ID') and not self.peek().value in ('{', '['):
|
|
772
586
|
class_name = self.consume('ID').value
|
|
773
587
|
args = []
|
|
774
588
|
while not self.check('NEWLINE') and not self.check('EOF'):
|
|
775
589
|
args.append(self.parse_expression())
|
|
776
|
-
|
|
777
590
|
self.consume('NEWLINE')
|
|
778
591
|
node = Instantiation(name, class_name, args)
|
|
779
592
|
node.line = token_is.line
|
|
780
593
|
return node
|
|
781
594
|
else:
|
|
782
|
-
# Support: name is "Alice" or data is {"x": 1}
|
|
783
595
|
value = self.parse_expression()
|
|
784
596
|
self.consume('NEWLINE')
|
|
785
597
|
node = Assign(name, value)
|
|
786
598
|
node.line = token_is.line
|
|
787
599
|
return node
|
|
788
|
-
|
|
789
600
|
elif self.check('DOT'):
|
|
790
|
-
# Method call or property access (or assignment)
|
|
791
601
|
self.consume('DOT')
|
|
792
602
|
member_token = self.consume()
|
|
793
603
|
member = member_token.value
|
|
794
|
-
# Warning: we accept ANY token as member if it follows DOT to allow keywords like 'json', 'open' etc.
|
|
795
|
-
|
|
796
604
|
if self.check('ASSIGN'):
|
|
797
605
|
self.consume('ASSIGN')
|
|
798
606
|
value = self.parse_expression()
|
|
799
607
|
self.consume('NEWLINE')
|
|
800
608
|
return PropertyAssign(name, member, value)
|
|
801
|
-
|
|
802
609
|
args = []
|
|
803
610
|
while not self.check('NEWLINE') and not self.check('EOF'):
|
|
804
611
|
args.append(self.parse_expression())
|
|
805
|
-
|
|
806
612
|
self.consume('NEWLINE')
|
|
807
613
|
node = MethodCall(name, member, args)
|
|
808
614
|
node.line = name_token.line
|
|
809
615
|
return node
|
|
810
|
-
|
|
811
616
|
else:
|
|
812
617
|
if not self.check('NEWLINE') and not self.check('EOF') and not self.check('EQ') and not self.check('IS'):
|
|
813
618
|
args = []
|
|
814
619
|
while not self.check('NEWLINE') and not self.check('EOF') and not self.check('IS'):
|
|
815
|
-
# Check for named arg: KEYWORD/ID = Expr
|
|
816
|
-
# Support HTML attributes like class=..., type=..., for=...
|
|
817
620
|
is_named_arg = False
|
|
818
621
|
if self.peek(1).type == 'ASSIGN':
|
|
819
|
-
# Acceptable keys
|
|
820
622
|
t_type = self.peek().type
|
|
821
623
|
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'):
|
|
822
624
|
is_named_arg = True
|
|
823
|
-
|
|
824
625
|
if is_named_arg:
|
|
825
626
|
key_token = self.consume()
|
|
826
627
|
key = key_token.value
|
|
827
628
|
self.consume('ASSIGN')
|
|
828
629
|
val = self.parse_expression()
|
|
829
|
-
# Pass as a dictionary node {key: val}
|
|
830
|
-
# Since Interpreter _make_tag_fn handles dicts as attrs
|
|
831
630
|
args.append(Dictionary([ (String(key), val) ]))
|
|
832
631
|
else:
|
|
833
632
|
if self.check('USING'):
|
|
834
633
|
self.consume('USING')
|
|
835
634
|
args.append(self.parse_expression())
|
|
836
|
-
|
|
837
635
|
if self.check('NEWLINE'):
|
|
838
636
|
self.consume('NEWLINE')
|
|
839
637
|
elif self.check('INDENT'):
|
|
840
638
|
pass
|
|
841
639
|
else:
|
|
842
640
|
self.consume('NEWLINE')
|
|
843
|
-
|
|
844
|
-
# Check for Block Call (WebDSL style)
|
|
845
|
-
# div class="x"
|
|
846
|
-
# p "hello"
|
|
847
641
|
body = None
|
|
848
642
|
if self.check('INDENT'):
|
|
849
643
|
self.consume('INDENT')
|
|
@@ -853,26 +647,18 @@ class Parser:
|
|
|
853
647
|
if self.check('DEDENT'): break
|
|
854
648
|
body.append(self.parse_statement())
|
|
855
649
|
self.consume('DEDENT')
|
|
856
|
-
|
|
857
650
|
node = Call(name, args, body)
|
|
858
651
|
node.line = name_token.line
|
|
859
652
|
return node
|
|
860
|
-
|
|
861
653
|
node = Call(name, args, body)
|
|
862
654
|
node.line = name_token.line
|
|
863
655
|
return node
|
|
864
|
-
|
|
865
656
|
if self.check('NEWLINE'):
|
|
866
657
|
self.consume('NEWLINE')
|
|
867
658
|
elif self.check('INDENT'):
|
|
868
659
|
pass
|
|
869
660
|
else:
|
|
870
661
|
self.consume('NEWLINE')
|
|
871
|
-
|
|
872
|
-
# Standalone variable/identifier -> Just access it (via Call with 0 args to check for Block)
|
|
873
|
-
# OR could be VarAccess.
|
|
874
|
-
# But if it has a BLOCK, it MUST be a call (e.g. div \n ...)
|
|
875
|
-
|
|
876
662
|
if self.check('INDENT'):
|
|
877
663
|
self.consume('INDENT')
|
|
878
664
|
body = []
|
|
@@ -881,13 +667,9 @@ class Parser:
|
|
|
881
667
|
if self.check('DEDENT'): break
|
|
882
668
|
body.append(self.parse_statement())
|
|
883
669
|
self.consume('DEDENT')
|
|
884
|
-
|
|
885
|
-
# Treat as Call(name, [], body)
|
|
886
670
|
node = Call(name, [], body)
|
|
887
671
|
node.line = name_token.line
|
|
888
672
|
return node
|
|
889
|
-
|
|
890
|
-
# Just access
|
|
891
673
|
node = VarAccess(name)
|
|
892
674
|
node.line = name_token.line
|
|
893
675
|
return node
|
|
@@ -896,63 +678,42 @@ class Parser:
|
|
|
896
678
|
token = self.consume('PRINT')
|
|
897
679
|
else:
|
|
898
680
|
token = self.consume('SAY')
|
|
899
|
-
|
|
900
|
-
# Check for 'show progress'
|
|
901
681
|
if self.check('PROGRESS'):
|
|
902
682
|
return self.parse_progress_loop(token)
|
|
903
|
-
|
|
904
683
|
style = None
|
|
905
684
|
color = None
|
|
906
|
-
|
|
907
|
-
# Handle 'say in red "..."'
|
|
908
685
|
if self.check('IN'):
|
|
909
686
|
self.consume('IN')
|
|
910
687
|
if self.peek().type in ('RED', 'GREEN', 'BLUE', 'YELLOW', 'CYAN', 'MAGENTA'):
|
|
911
688
|
color = self.consume().value
|
|
912
|
-
|
|
913
|
-
# Handle 'say bold green "..."'
|
|
914
689
|
if self.check('BOLD'):
|
|
915
690
|
self.consume('BOLD')
|
|
916
691
|
style = 'bold'
|
|
917
|
-
|
|
918
692
|
if self.peek().type in ('RED', 'GREEN', 'BLUE', 'YELLOW', 'CYAN', 'MAGENTA'):
|
|
919
693
|
color = self.consume().value
|
|
920
|
-
|
|
921
694
|
expr = self.parse_expression()
|
|
922
695
|
self.consume('NEWLINE')
|
|
923
696
|
node = Print(expression=expr, style=style, color=color)
|
|
924
697
|
node.line = token.line
|
|
925
698
|
return node
|
|
926
|
-
|
|
927
699
|
def parse_progress_loop(self, start_token: Token) -> ProgressLoop:
|
|
928
|
-
"""Parse: show progress for i in ..."""
|
|
929
700
|
self.consume('PROGRESS')
|
|
930
|
-
|
|
931
|
-
# Expect a loop like 'for ...' or 'repeat ...'
|
|
932
|
-
# But 'for' parser expects to be called when current token is 'FOR'
|
|
933
701
|
if not (self.check('FOR') or self.check('REPEAT') or self.check('LOOP')):
|
|
934
702
|
raise SyntaxError(f"Expected loop after 'show progress' on line {start_token.line}")
|
|
935
|
-
|
|
936
703
|
if self.check('FOR') or self.check('LOOP'):
|
|
937
704
|
loop_node = self.parse_for()
|
|
938
705
|
else:
|
|
939
706
|
loop_node = self.parse_repeat()
|
|
940
|
-
|
|
941
707
|
node = ProgressLoop(loop_node)
|
|
942
708
|
node.line = start_token.line
|
|
943
709
|
return node
|
|
944
|
-
|
|
945
710
|
def parse_serve(self) -> ServeStatic:
|
|
946
|
-
"""Parse: serve static 'folder' at 'url' OR serve files from 'folder'"""
|
|
947
711
|
token = self.consume('SERVE')
|
|
948
|
-
|
|
949
|
-
# Natural syntax: serve files from "public"
|
|
950
712
|
if self.check('FILES'):
|
|
951
713
|
self.consume('FILES')
|
|
952
714
|
if self.check('FROM'):
|
|
953
715
|
self.consume('FROM')
|
|
954
716
|
folder = self.parse_expression()
|
|
955
|
-
# Default URL is /static
|
|
956
717
|
url = String('/static')
|
|
957
718
|
if self.check('AT'):
|
|
958
719
|
self.consume('AT')
|
|
@@ -963,8 +724,6 @@ class Parser:
|
|
|
963
724
|
node = ServeStatic(folder, url)
|
|
964
725
|
node.line = token.line
|
|
965
726
|
return node
|
|
966
|
-
|
|
967
|
-
# Original syntax: serve static "folder" at "/url"
|
|
968
727
|
self.consume('STATIC')
|
|
969
728
|
folder = self.parse_expression()
|
|
970
729
|
self.consume('AT')
|
|
@@ -973,26 +732,18 @@ class Parser:
|
|
|
973
732
|
node = ServeStatic(folder, url)
|
|
974
733
|
node.line = token.line
|
|
975
734
|
return node
|
|
976
|
-
|
|
977
735
|
def parse_listen(self) -> Listen:
|
|
978
|
-
"""Parse: listen on port 8000"""
|
|
979
736
|
token = self.consume('LISTEN')
|
|
980
|
-
|
|
981
737
|
if self.check('ON'): self.consume('ON')
|
|
982
738
|
if self.check('PORT'): self.consume('PORT')
|
|
983
|
-
|
|
984
739
|
port_num = self.parse_expression()
|
|
985
740
|
self.consume('NEWLINE')
|
|
986
|
-
|
|
987
741
|
node = Listen(port_num)
|
|
988
742
|
node.line = token.line
|
|
989
743
|
return node
|
|
990
|
-
|
|
991
744
|
def parse_every(self) -> Every:
|
|
992
|
-
"""Parse: every 5 minutes (body)"""
|
|
993
745
|
token = self.consume('EVERY')
|
|
994
746
|
interval = self.parse_expression()
|
|
995
|
-
|
|
996
747
|
unit = 'seconds'
|
|
997
748
|
if self.check('MINUTE'):
|
|
998
749
|
self.consume()
|
|
@@ -1000,26 +751,20 @@ class Parser:
|
|
|
1000
751
|
elif self.check('SECOND'):
|
|
1001
752
|
self.consume()
|
|
1002
753
|
unit = 'seconds'
|
|
1003
|
-
|
|
1004
754
|
self.consume('NEWLINE')
|
|
1005
755
|
self.consume('INDENT')
|
|
1006
|
-
|
|
1007
756
|
body = []
|
|
1008
757
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1009
758
|
while self.check('NEWLINE'): self.consume()
|
|
1010
759
|
if self.check('DEDENT'): break
|
|
1011
760
|
body.append(self.parse_statement())
|
|
1012
|
-
|
|
1013
761
|
self.consume('DEDENT')
|
|
1014
762
|
node = Every(interval, unit, body)
|
|
1015
763
|
node.line = token.line
|
|
1016
764
|
return node
|
|
1017
|
-
|
|
1018
765
|
def parse_after(self) -> After:
|
|
1019
|
-
"""Parse: in 5 minutes (body)"""
|
|
1020
766
|
token = self.consume('IN')
|
|
1021
767
|
delay = self.parse_expression()
|
|
1022
|
-
|
|
1023
768
|
unit = 'seconds'
|
|
1024
769
|
if self.check('MINUTE'):
|
|
1025
770
|
self.consume()
|
|
@@ -1027,32 +772,22 @@ class Parser:
|
|
|
1027
772
|
elif self.check('SECOND'):
|
|
1028
773
|
self.consume()
|
|
1029
774
|
unit = 'seconds'
|
|
1030
|
-
|
|
1031
775
|
self.consume('NEWLINE')
|
|
1032
776
|
self.consume('INDENT')
|
|
1033
|
-
|
|
1034
777
|
body = []
|
|
1035
778
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1036
779
|
while self.check('NEWLINE'): self.consume()
|
|
1037
780
|
if self.check('DEDENT'): break
|
|
1038
781
|
body.append(self.parse_statement())
|
|
1039
|
-
|
|
1040
782
|
self.consume('DEDENT')
|
|
1041
783
|
node = After(delay, unit, body)
|
|
1042
784
|
node.line = token.line
|
|
1043
785
|
return node
|
|
1044
|
-
|
|
1045
|
-
# === NATURAL ENGLISH WEB DSL PARSERS ===
|
|
1046
|
-
|
|
1047
786
|
def parse_define_page(self) -> Node:
|
|
1048
|
-
"""Parse: define page Name (using args) = body"""
|
|
1049
787
|
token = self.consume('DEFINE')
|
|
1050
788
|
if self.check('PAGE'):
|
|
1051
789
|
self.consume('PAGE')
|
|
1052
|
-
|
|
1053
790
|
name = self.consume('ID').value
|
|
1054
|
-
|
|
1055
|
-
# Check for arguments: define page TaskList using items
|
|
1056
791
|
args = []
|
|
1057
792
|
if self.check('USING'):
|
|
1058
793
|
self.consume('USING')
|
|
@@ -1060,95 +795,67 @@ class Parser:
|
|
|
1060
795
|
while self.check('COMMA'):
|
|
1061
796
|
self.consume('COMMA')
|
|
1062
797
|
args.append((self.consume('ID').value, None, None))
|
|
1063
|
-
|
|
1064
798
|
self.consume('NEWLINE')
|
|
1065
799
|
self.consume('INDENT')
|
|
1066
|
-
|
|
1067
800
|
body = []
|
|
1068
801
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1069
802
|
while self.check('NEWLINE'): self.consume()
|
|
1070
803
|
if self.check('DEDENT'): break
|
|
1071
804
|
body.append(self.parse_statement())
|
|
1072
805
|
self.consume('DEDENT')
|
|
1073
|
-
|
|
1074
|
-
# Create a FunctionDef node (reuse existing infrastructure)
|
|
1075
806
|
node = FunctionDef(name, args, body)
|
|
1076
807
|
node.line = token.line
|
|
1077
808
|
return node
|
|
1078
|
-
|
|
1079
809
|
def parse_add_to(self) -> Node:
|
|
1080
|
-
"""Parse: add item to list"""
|
|
1081
810
|
token = self.consume('ADD')
|
|
1082
811
|
item_expr = self.parse_factor_simple()
|
|
1083
|
-
|
|
1084
812
|
if self.check('TO') or self.check('INTO'):
|
|
1085
813
|
self.consume()
|
|
1086
|
-
|
|
1087
814
|
list_name = self.consume('ID').value
|
|
1088
815
|
self.consume('NEWLINE')
|
|
1089
|
-
|
|
1090
|
-
# Generate: list = list + [item]
|
|
1091
816
|
list_access = VarAccess(list_name)
|
|
1092
817
|
item_list = ListVal([item_expr])
|
|
1093
818
|
concat = BinOp(list_access, '+', item_list)
|
|
1094
819
|
node = Assign(list_name, concat)
|
|
1095
820
|
node.line = token.line
|
|
1096
821
|
return node
|
|
1097
|
-
|
|
1098
822
|
def parse_start_server(self) -> Node:
|
|
1099
|
-
"""Parse: start server (on port X)"""
|
|
1100
823
|
token = self.consume('START')
|
|
1101
824
|
if self.check('SERVER'):
|
|
1102
825
|
self.consume('SERVER')
|
|
1103
|
-
|
|
1104
|
-
# Default port 8080
|
|
1105
826
|
port = Number(8080)
|
|
1106
|
-
|
|
1107
827
|
if self.check('ON'):
|
|
1108
828
|
self.consume('ON')
|
|
1109
829
|
if self.check('PORT'):
|
|
1110
830
|
self.consume('PORT')
|
|
1111
831
|
port = self.parse_expression()
|
|
1112
|
-
|
|
1113
832
|
self.consume('NEWLINE')
|
|
1114
|
-
|
|
1115
833
|
node = Listen(port)
|
|
1116
834
|
node.line = token.line
|
|
1117
835
|
return node
|
|
1118
|
-
|
|
1119
836
|
def parse_heading(self) -> Node:
|
|
1120
|
-
"""Parse: heading 'text' -> h1"""
|
|
1121
837
|
token = self.consume('HEADING')
|
|
1122
838
|
text = self.parse_expression()
|
|
1123
839
|
self.consume('NEWLINE')
|
|
1124
|
-
|
|
1125
|
-
# Create a Call node for 'h1' builtin
|
|
1126
840
|
node = Call('h1', [text])
|
|
1127
841
|
node.line = token.line
|
|
1128
842
|
return node
|
|
1129
|
-
|
|
1130
843
|
def parse_paragraph(self) -> Node:
|
|
1131
|
-
"""Parse: paragraph 'text' -> p"""
|
|
1132
844
|
token = self.consume('PARAGRAPH')
|
|
1133
845
|
text = self.parse_expression()
|
|
1134
846
|
self.consume('NEWLINE')
|
|
1135
|
-
|
|
1136
|
-
# Create a Call node for 'p' builtin
|
|
1137
847
|
node = Call('p', [text])
|
|
1138
848
|
node.line = token.line
|
|
1139
849
|
return node
|
|
1140
|
-
|
|
1141
850
|
def parse_assign(self) -> Assign:
|
|
1142
851
|
name = self.consume('ID').value
|
|
1143
852
|
self.consume('ASSIGN')
|
|
1144
853
|
value = self.parse_expression()
|
|
1145
854
|
self.consume('NEWLINE')
|
|
1146
855
|
return Assign(name, value)
|
|
1147
|
-
|
|
1148
856
|
def parse_import(self) -> Node:
|
|
1149
857
|
token = self.consume('USE')
|
|
1150
858
|
path = self.consume('STRING').value
|
|
1151
|
-
|
|
1152
859
|
if self.check('AS'):
|
|
1153
860
|
self.consume('AS')
|
|
1154
861
|
alias = self.consume('ID').value
|
|
@@ -1157,38 +864,22 @@ class Parser:
|
|
|
1157
864
|
else:
|
|
1158
865
|
self.consume('NEWLINE')
|
|
1159
866
|
node = Import(path)
|
|
1160
|
-
|
|
1161
867
|
node.line = token.line
|
|
1162
868
|
return node
|
|
1163
|
-
|
|
1164
869
|
def parse_if(self) -> If:
|
|
1165
870
|
self.consume('IF')
|
|
1166
871
|
condition = self.parse_expression()
|
|
1167
872
|
self.consume('NEWLINE')
|
|
1168
873
|
self.consume('INDENT')
|
|
1169
|
-
|
|
1170
874
|
body = []
|
|
1171
875
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1172
876
|
while self.check('NEWLINE'): self.consume()
|
|
1173
877
|
if self.check('DEDENT'): break
|
|
1174
878
|
body.append(self.parse_statement())
|
|
1175
|
-
|
|
1176
879
|
self.consume('DEDENT')
|
|
1177
|
-
|
|
1178
880
|
else_body = None
|
|
1179
|
-
|
|
1180
|
-
# Handle ELIF (as recursive If in else_body? Or flat? Let's use recursive for simplicity with AST)
|
|
1181
|
-
# AST is If(cond, body, else_body).
|
|
1182
|
-
# ELIF cond body -> else_body = [If(cond, body, ...)]
|
|
1183
|
-
|
|
1184
881
|
if self.check('ELIF'):
|
|
1185
|
-
# This 'elif' becomes the 'if' of the else_body
|
|
1186
|
-
# But wait, 'elif' token needs to be consumed inside the recursive call?
|
|
1187
|
-
# Or we recursively call parse_if but trick it?
|
|
1188
|
-
# Better: Rewrite parse_if to NOT consume IF if called recursively?
|
|
1189
|
-
# No, standard way:
|
|
1190
882
|
else_body = [self.parse_elif()]
|
|
1191
|
-
|
|
1192
883
|
elif self.check('ELSE'):
|
|
1193
884
|
self.consume('ELSE')
|
|
1194
885
|
self.consume('NEWLINE')
|
|
@@ -1199,23 +890,18 @@ class Parser:
|
|
|
1199
890
|
if self.check('DEDENT'): break
|
|
1200
891
|
else_body.append(self.parse_statement())
|
|
1201
892
|
self.consume('DEDENT')
|
|
1202
|
-
|
|
1203
893
|
return If(condition, body, else_body)
|
|
1204
|
-
|
|
1205
894
|
def parse_elif(self) -> If:
|
|
1206
|
-
# Similar to parse_if but consumes ELIF
|
|
1207
895
|
token = self.consume('ELIF')
|
|
1208
896
|
condition = self.parse_expression()
|
|
1209
897
|
self.consume('NEWLINE')
|
|
1210
898
|
self.consume('INDENT')
|
|
1211
|
-
|
|
1212
899
|
body = []
|
|
1213
900
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1214
901
|
while self.check('NEWLINE'): self.consume()
|
|
1215
902
|
if self.check('DEDENT'): break
|
|
1216
903
|
body.append(self.parse_statement())
|
|
1217
904
|
self.consume('DEDENT')
|
|
1218
|
-
|
|
1219
905
|
else_body = None
|
|
1220
906
|
if self.check('ELIF'):
|
|
1221
907
|
else_body = [self.parse_elif()]
|
|
@@ -1229,55 +915,45 @@ class Parser:
|
|
|
1229
915
|
if self.check('DEDENT'): break
|
|
1230
916
|
else_body.append(self.parse_statement())
|
|
1231
917
|
self.consume('DEDENT')
|
|
1232
|
-
|
|
1233
918
|
node = If(condition, body, else_body)
|
|
1234
919
|
node.line = token.line
|
|
1235
920
|
node = If(condition, body, else_body)
|
|
1236
921
|
node.line = token.line
|
|
1237
922
|
return node
|
|
1238
|
-
|
|
1239
923
|
def parse_while(self) -> While:
|
|
1240
924
|
start_token = self.consume('WHILE')
|
|
1241
925
|
condition = self.parse_expression()
|
|
1242
926
|
self.consume('NEWLINE')
|
|
1243
927
|
self.consume('INDENT')
|
|
1244
|
-
|
|
1245
928
|
body = []
|
|
1246
929
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1247
930
|
while self.check('NEWLINE'): self.consume()
|
|
1248
931
|
if self.check('DEDENT'): break
|
|
1249
932
|
body.append(self.parse_statement())
|
|
1250
|
-
|
|
1251
933
|
self.consume('DEDENT')
|
|
1252
934
|
node = While(condition, body)
|
|
1253
935
|
node.line = start_token.line
|
|
1254
936
|
return node
|
|
1255
|
-
|
|
1256
937
|
def parse_try(self) -> Try:
|
|
1257
938
|
start_token = self.consume('TRY')
|
|
1258
939
|
self.consume('NEWLINE')
|
|
1259
940
|
self.consume('INDENT')
|
|
1260
|
-
|
|
1261
941
|
try_body = []
|
|
1262
942
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1263
943
|
while self.check('NEWLINE'): self.consume()
|
|
1264
944
|
if self.check('DEDENT'): break
|
|
1265
945
|
try_body.append(self.parse_statement())
|
|
1266
946
|
self.consume('DEDENT')
|
|
1267
|
-
|
|
1268
947
|
self.consume('CATCH')
|
|
1269
948
|
catch_var = self.consume('ID').value
|
|
1270
949
|
self.consume('NEWLINE')
|
|
1271
950
|
self.consume('INDENT')
|
|
1272
|
-
|
|
1273
951
|
catch_body = []
|
|
1274
952
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1275
953
|
while self.check('NEWLINE'): self.consume()
|
|
1276
954
|
if self.check('DEDENT'): break
|
|
1277
955
|
catch_body.append(self.parse_statement())
|
|
1278
956
|
self.consume('DEDENT')
|
|
1279
|
-
|
|
1280
|
-
# Check for always block (finally)
|
|
1281
957
|
always_body = []
|
|
1282
958
|
if self.check('ALWAYS'):
|
|
1283
959
|
self.consume('ALWAYS')
|
|
@@ -1288,63 +964,48 @@ class Parser:
|
|
|
1288
964
|
if self.check('DEDENT'): break
|
|
1289
965
|
always_body.append(self.parse_statement())
|
|
1290
966
|
self.consume('DEDENT')
|
|
1291
|
-
|
|
1292
967
|
if always_body:
|
|
1293
968
|
node = TryAlways(try_body, catch_var, catch_body, always_body)
|
|
1294
969
|
else:
|
|
1295
970
|
node = Try(try_body, catch_var, catch_body)
|
|
1296
971
|
node.line = start_token.line
|
|
1297
972
|
return node
|
|
1298
|
-
|
|
1299
973
|
def parse_list(self) -> Node:
|
|
1300
974
|
token = self.consume('LBRACKET')
|
|
1301
|
-
|
|
1302
975
|
def skip_formatted():
|
|
1303
976
|
while self.check('NEWLINE') or self.check('INDENT') or self.check('DEDENT'):
|
|
1304
977
|
self.consume()
|
|
1305
978
|
skip_formatted()
|
|
1306
|
-
# Empty list
|
|
1307
979
|
if self.check('RBRACKET'):
|
|
1308
980
|
self.consume('RBRACKET')
|
|
1309
981
|
node = ListVal([])
|
|
1310
982
|
node.line = token.line
|
|
1311
983
|
return node
|
|
1312
|
-
|
|
1313
|
-
# Check for spread operator
|
|
1314
984
|
if self.check('DOTDOTDOT'):
|
|
1315
985
|
node = self._parse_list_with_spread(token)
|
|
1316
986
|
skip_formatted()
|
|
1317
987
|
return node
|
|
1318
|
-
|
|
1319
|
-
# Parse first expression
|
|
1320
988
|
first_expr = self.parse_expression()
|
|
1321
989
|
skip_formatted()
|
|
1322
|
-
|
|
1323
|
-
# Check for list comprehension: [expr for var in iterable]
|
|
1324
990
|
if self.check('FOR'):
|
|
1325
991
|
self.consume('FOR')
|
|
1326
992
|
var_name = self.consume('ID').value
|
|
1327
993
|
self.consume('IN')
|
|
1328
994
|
iterable = self.parse_expression()
|
|
1329
|
-
|
|
1330
|
-
# Optional condition: [x for x in list if x > 0]
|
|
1331
995
|
condition = None
|
|
1332
996
|
if self.check('IF'):
|
|
1333
997
|
self.consume('IF')
|
|
1334
998
|
condition = self.parse_expression()
|
|
1335
|
-
|
|
1336
999
|
self.consume('RBRACKET')
|
|
1337
1000
|
node = ListComprehension(first_expr, var_name, iterable, condition)
|
|
1338
1001
|
node.line = token.line
|
|
1339
1002
|
return node
|
|
1340
|
-
|
|
1341
|
-
# Regular list
|
|
1342
1003
|
elements = [first_expr]
|
|
1343
1004
|
while self.check('COMMA'):
|
|
1344
1005
|
self.consume('COMMA')
|
|
1345
1006
|
skip_formatted()
|
|
1346
1007
|
if self.check('RBRACKET'):
|
|
1347
|
-
break
|
|
1008
|
+
break
|
|
1348
1009
|
if self.check('DOTDOTDOT'):
|
|
1349
1010
|
self.consume('DOTDOTDOT')
|
|
1350
1011
|
spread_val = self.parse_expression()
|
|
@@ -1354,22 +1015,18 @@ class Parser:
|
|
|
1354
1015
|
else:
|
|
1355
1016
|
elements.append(self.parse_expression())
|
|
1356
1017
|
skip_formatted()
|
|
1357
|
-
|
|
1358
1018
|
skip_formatted()
|
|
1359
1019
|
self.consume('RBRACKET')
|
|
1360
1020
|
node = ListVal(elements)
|
|
1361
1021
|
node.line = token.line
|
|
1362
1022
|
return node
|
|
1363
|
-
|
|
1364
1023
|
def _parse_list_with_spread(self, token: Token) -> ListVal:
|
|
1365
|
-
"""Parse list starting with spread operator"""
|
|
1366
1024
|
elements = []
|
|
1367
1025
|
self.consume('DOTDOTDOT')
|
|
1368
1026
|
spread_val = self.parse_expression()
|
|
1369
1027
|
spread_node = Spread(spread_val)
|
|
1370
1028
|
spread_node.line = token.line
|
|
1371
1029
|
elements.append(spread_node)
|
|
1372
|
-
|
|
1373
1030
|
while self.check('COMMA'):
|
|
1374
1031
|
self.consume('COMMA')
|
|
1375
1032
|
if self.check('RBRACKET'):
|
|
@@ -1382,62 +1039,50 @@ class Parser:
|
|
|
1382
1039
|
elements.append(spread_node)
|
|
1383
1040
|
else:
|
|
1384
1041
|
elements.append(self.parse_expression())
|
|
1385
|
-
|
|
1386
1042
|
self.consume('RBRACKET')
|
|
1387
1043
|
node = ListVal(elements)
|
|
1388
1044
|
node.line = token.line
|
|
1389
1045
|
return node
|
|
1390
|
-
|
|
1391
1046
|
def parse_dict(self) -> Dictionary:
|
|
1392
1047
|
token = self.consume('LBRACE')
|
|
1393
|
-
|
|
1394
1048
|
def skip_formatted():
|
|
1395
1049
|
while self.check('NEWLINE') or self.check('INDENT') or self.check('DEDENT'):
|
|
1396
1050
|
self.consume()
|
|
1397
|
-
|
|
1398
1051
|
skip_formatted()
|
|
1399
1052
|
pairs = []
|
|
1400
1053
|
if not self.check('RBRACE'):
|
|
1401
|
-
# Support { key: value } or { "key": value } or { expr: value }
|
|
1402
1054
|
if self.check('ID') and self.peek(1).type == 'COLON':
|
|
1403
1055
|
key_token = self.consume('ID')
|
|
1404
1056
|
key = String(key_token.value)
|
|
1405
1057
|
key.line = key_token.line
|
|
1406
1058
|
else:
|
|
1407
1059
|
key = self.parse_expression()
|
|
1408
|
-
|
|
1409
1060
|
self.consume('COLON')
|
|
1410
1061
|
skip_formatted()
|
|
1411
1062
|
value = self.parse_expression()
|
|
1412
1063
|
pairs.append((key, value))
|
|
1413
1064
|
skip_formatted()
|
|
1414
|
-
|
|
1415
1065
|
while self.check('COMMA'):
|
|
1416
1066
|
self.consume('COMMA')
|
|
1417
1067
|
skip_formatted()
|
|
1418
1068
|
if self.check('RBRACE'): break
|
|
1419
|
-
|
|
1420
1069
|
if self.check('ID') and self.peek(1).type == 'COLON':
|
|
1421
1070
|
key_token = self.consume('ID')
|
|
1422
1071
|
key = String(key_token.value)
|
|
1423
1072
|
key.line = key_token.line
|
|
1424
1073
|
else:
|
|
1425
1074
|
key = self.parse_expression()
|
|
1426
|
-
|
|
1427
1075
|
self.consume('COLON')
|
|
1428
1076
|
skip_formatted()
|
|
1429
1077
|
value = self.parse_expression()
|
|
1430
1078
|
pairs.append((key, value))
|
|
1431
1079
|
skip_formatted()
|
|
1432
|
-
|
|
1433
1080
|
skip_formatted()
|
|
1434
1081
|
self.consume('RBRACE')
|
|
1435
1082
|
node = Dictionary(pairs)
|
|
1436
1083
|
node.line = token.line
|
|
1437
1084
|
return node
|
|
1438
|
-
|
|
1439
1085
|
def parse_factor_simple(self) -> Node:
|
|
1440
|
-
"""Parse a simple factor (atomic) to be used as an argument."""
|
|
1441
1086
|
token = self.peek()
|
|
1442
1087
|
if token.type == 'NUMBER':
|
|
1443
1088
|
self.consume()
|
|
@@ -1464,26 +1109,18 @@ class Parser:
|
|
|
1464
1109
|
expr = String(part)
|
|
1465
1110
|
expr.line = token.line
|
|
1466
1111
|
else:
|
|
1467
|
-
# Full expression support via re-parsing
|
|
1468
1112
|
snippet = part.strip()
|
|
1469
1113
|
if snippet:
|
|
1470
1114
|
sub_lexer = Lexer(snippet)
|
|
1471
|
-
# Remove comments/indent processing if any? Expressions are usually simple.
|
|
1472
1115
|
sub_tokens = sub_lexer.tokenize()
|
|
1473
|
-
# Tokenize adds EOF. Parser expects list of tokens.
|
|
1474
|
-
|
|
1475
1116
|
sub_parser = Parser(sub_tokens)
|
|
1476
|
-
# We want a single expression.
|
|
1477
|
-
# But parse_expression might fail if tokens are empty/weird.
|
|
1478
1117
|
try:
|
|
1479
1118
|
expr = sub_parser.parse_expression()
|
|
1480
1119
|
expr.line = token.line
|
|
1481
1120
|
except Exception as e:
|
|
1482
|
-
# Fallback or error?
|
|
1483
1121
|
raise SyntaxError(f"Invalid interpolation expression: '{snippet}' on line {token.line}")
|
|
1484
1122
|
else:
|
|
1485
1123
|
continue
|
|
1486
|
-
|
|
1487
1124
|
if current_node is None:
|
|
1488
1125
|
current_node = expr
|
|
1489
1126
|
else:
|
|
@@ -1509,7 +1146,6 @@ class Parser:
|
|
|
1509
1146
|
return self.parse_dict()
|
|
1510
1147
|
elif token.type == 'ID':
|
|
1511
1148
|
self.consume()
|
|
1512
|
-
# Dont check for args here, just VarAccess or Dot
|
|
1513
1149
|
if self.check('DOT'):
|
|
1514
1150
|
self.consume('DOT')
|
|
1515
1151
|
prop_token = self.consume()
|
|
@@ -1526,7 +1162,6 @@ class Parser:
|
|
|
1526
1162
|
self.consume('RPAREN')
|
|
1527
1163
|
return expr
|
|
1528
1164
|
elif token.type == 'INPUT' or token.type == 'ASK':
|
|
1529
|
-
# Check if this is 'input type="..."' i.e. HTML tag
|
|
1530
1165
|
is_tag = False
|
|
1531
1166
|
next_t = self.peek(1)
|
|
1532
1167
|
if next_t.type in ('ID', 'TYPE', 'STRING', 'NAME', 'VALUE', 'CLASS', 'STYLE', 'ONCLICK', 'SRC', 'HREF', 'ACTION', 'METHOD'):
|
|
@@ -1540,12 +1175,9 @@ class Parser:
|
|
|
1540
1175
|
node = Input(prompt)
|
|
1541
1176
|
node.line = token.line
|
|
1542
1177
|
return node
|
|
1543
|
-
|
|
1544
1178
|
raise SyntaxError(f"Unexpected argument token {token.type} at line {token.line}")
|
|
1545
|
-
|
|
1546
1179
|
def parse_factor(self) -> Node:
|
|
1547
1180
|
token = self.peek()
|
|
1548
|
-
|
|
1549
1181
|
if token.type == 'NOT':
|
|
1550
1182
|
op = self.consume()
|
|
1551
1183
|
right = self.parse_factor()
|
|
@@ -1562,15 +1194,11 @@ class Parser:
|
|
|
1562
1194
|
return node
|
|
1563
1195
|
elif token.type == 'EXECUTE':
|
|
1564
1196
|
op = self.consume()
|
|
1565
|
-
# run "cmd" -> convert to function call run("cmd")
|
|
1566
|
-
# Argument is parse_factor or parse_expression?
|
|
1567
|
-
# run "cmd". parse_expression catches "cmd".
|
|
1568
1197
|
right = self.parse_expression()
|
|
1569
1198
|
node = Call('run', [right])
|
|
1570
1199
|
node.line = op.line
|
|
1571
1200
|
return node
|
|
1572
1201
|
elif token.type == 'COUNT' or token.type == 'HOW':
|
|
1573
|
-
# count of x, how many x
|
|
1574
1202
|
token = self.consume()
|
|
1575
1203
|
if token.type == 'HOW':
|
|
1576
1204
|
self.consume('MANY')
|
|
@@ -1589,10 +1217,9 @@ class Parser:
|
|
|
1589
1217
|
elif token.type == 'CONVERT':
|
|
1590
1218
|
return self.parse_convert()
|
|
1591
1219
|
elif token.type == 'LOAD' and self.peek(1).type == 'CSV':
|
|
1592
|
-
# Handle load csv as expression
|
|
1593
1220
|
self.consume('LOAD')
|
|
1594
1221
|
self.consume('CSV')
|
|
1595
|
-
path = self.parse_factor()
|
|
1222
|
+
path = self.parse_factor()
|
|
1596
1223
|
node = CsvOp('load', None, path)
|
|
1597
1224
|
node.line = token.line
|
|
1598
1225
|
return node
|
|
@@ -1621,7 +1248,6 @@ class Parser:
|
|
|
1621
1248
|
node = DateOp('today')
|
|
1622
1249
|
node.line = token.line
|
|
1623
1250
|
return node
|
|
1624
|
-
|
|
1625
1251
|
if token.type == 'NUMBER':
|
|
1626
1252
|
self.consume()
|
|
1627
1253
|
val = token.value
|
|
@@ -1656,41 +1282,30 @@ class Parser:
|
|
|
1656
1282
|
elif token.type == 'LBRACE':
|
|
1657
1283
|
return self.parse_dict()
|
|
1658
1284
|
elif token.type == 'ID':
|
|
1659
|
-
# Check for Natural Collection Syntax: a list of / a unique set of
|
|
1660
1285
|
if token.value == 'a':
|
|
1661
1286
|
if self.peek(1).type == 'LIST' and self.peek(2).type == 'OF':
|
|
1662
|
-
# a list of ...
|
|
1663
1287
|
return self._parse_natural_list()
|
|
1664
1288
|
elif self.peek(1).type == 'UNIQUE' and self.peek(2).type == 'SET' and self.peek(3).type == 'OF':
|
|
1665
|
-
# a unique set of ...
|
|
1666
1289
|
return self._parse_natural_set()
|
|
1667
|
-
|
|
1668
1290
|
self.consume()
|
|
1669
1291
|
instance_name = token.value
|
|
1670
1292
|
method_name = None
|
|
1671
|
-
|
|
1672
|
-
# Check for dot access in expression
|
|
1673
1293
|
if self.check('DOT'):
|
|
1674
1294
|
self.consume('DOT')
|
|
1675
1295
|
method_name = self.consume().value
|
|
1676
|
-
|
|
1677
1296
|
args = []
|
|
1678
1297
|
force_call = False
|
|
1679
|
-
|
|
1680
1298
|
while True:
|
|
1681
1299
|
next_t = self.peek()
|
|
1682
|
-
|
|
1683
1300
|
if next_t.type == 'LPAREN' and self.peek(1).type == 'RPAREN':
|
|
1684
1301
|
self.consume('LPAREN')
|
|
1685
1302
|
self.consume('RPAREN')
|
|
1686
1303
|
force_call = True
|
|
1687
1304
|
continue
|
|
1688
|
-
|
|
1689
1305
|
if next_t.type in ('NUMBER', 'STRING', 'REGEX', 'ID', 'LPAREN', 'INPUT', 'ASK', 'YES', 'NO', 'LBRACKET', 'LBRACE'):
|
|
1690
1306
|
args.append(self.parse_factor_simple())
|
|
1691
1307
|
else:
|
|
1692
1308
|
break
|
|
1693
|
-
|
|
1694
1309
|
if method_name:
|
|
1695
1310
|
if args or force_call:
|
|
1696
1311
|
node = MethodCall(instance_name, method_name, args)
|
|
@@ -1698,12 +1313,10 @@ class Parser:
|
|
|
1698
1313
|
node = PropertyAccess(instance_name, method_name)
|
|
1699
1314
|
node.line = token.line
|
|
1700
1315
|
return node
|
|
1701
|
-
|
|
1702
1316
|
if args or force_call:
|
|
1703
1317
|
node = Call(instance_name, args)
|
|
1704
1318
|
node.line = token.line
|
|
1705
1319
|
return node
|
|
1706
|
-
|
|
1707
1320
|
node = VarAccess(instance_name)
|
|
1708
1321
|
node.line = token.line
|
|
1709
1322
|
return node
|
|
@@ -1713,11 +1326,9 @@ class Parser:
|
|
|
1713
1326
|
self.consume('RPAREN')
|
|
1714
1327
|
return expr
|
|
1715
1328
|
elif token.type == 'INPUT' or token.type == 'ASK':
|
|
1716
|
-
# Check if this is 'input type="..."' i.e. HTML tag
|
|
1717
1329
|
next_t = self.peek(1)
|
|
1718
1330
|
if next_t.type in ('ID', 'TYPE', 'STRING', 'NAME', 'VALUE', 'CLASS', 'STYLE', 'ONCLICK', 'SRC', 'HREF', 'ACTION', 'METHOD'):
|
|
1719
|
-
|
|
1720
|
-
self.consume() # Consume INPUT token
|
|
1331
|
+
self.consume()
|
|
1721
1332
|
return self.parse_id_start_statement(passed_name_token=token)
|
|
1722
1333
|
self.consume()
|
|
1723
1334
|
prompt = None
|
|
@@ -1728,7 +1339,7 @@ class Parser:
|
|
|
1728
1339
|
return node
|
|
1729
1340
|
elif token.type == 'PROMPT':
|
|
1730
1341
|
self.consume()
|
|
1731
|
-
prompt_expr = self.parse_factor()
|
|
1342
|
+
prompt_expr = self.parse_factor()
|
|
1732
1343
|
node = Prompt(prompt_expr)
|
|
1733
1344
|
node.line = token.line
|
|
1734
1345
|
return node
|
|
@@ -1738,138 +1349,93 @@ class Parser:
|
|
|
1738
1349
|
node = Confirm(prompt_expr)
|
|
1739
1350
|
node.line = token.line
|
|
1740
1351
|
return node
|
|
1741
|
-
|
|
1742
1352
|
raise SyntaxError(f"Unexpected token {token.type} at line {token.line}")
|
|
1743
|
-
|
|
1744
1353
|
def parse_for(self) -> Node:
|
|
1745
|
-
# Support:
|
|
1746
|
-
# 1. for x in list -> ForIn loop
|
|
1747
|
-
# 2. for i in range 1 10 -> For loop with range
|
|
1748
|
-
# 3. for 20 in range -> For loop (old style)
|
|
1749
|
-
# 4. loop 20 times -> For loop
|
|
1750
|
-
|
|
1751
1354
|
if self.check('LOOP'):
|
|
1752
|
-
# loop N times
|
|
1753
1355
|
start_token = self.consume('LOOP')
|
|
1754
1356
|
count_expr = self.parse_expression()
|
|
1755
1357
|
self.consume('TIMES')
|
|
1756
|
-
|
|
1757
1358
|
self.consume('NEWLINE')
|
|
1758
1359
|
self.consume('INDENT')
|
|
1759
|
-
|
|
1760
1360
|
body = []
|
|
1761
1361
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1762
1362
|
while self.check('NEWLINE'): self.consume()
|
|
1763
1363
|
if self.check('DEDENT'): break
|
|
1764
1364
|
body.append(self.parse_statement())
|
|
1765
|
-
|
|
1766
1365
|
self.consume('DEDENT')
|
|
1767
1366
|
node = For(count_expr, body)
|
|
1768
1367
|
node.line = start_token.line
|
|
1769
1368
|
return node
|
|
1770
|
-
|
|
1771
|
-
# for ...
|
|
1772
1369
|
start_token = self.consume('FOR')
|
|
1773
|
-
|
|
1774
|
-
# Check if it's: for VAR in ITERABLE (where VAR is an ID followed by IN)
|
|
1775
1370
|
if self.check('ID') and self.peek(1).type == 'IN':
|
|
1776
1371
|
var_name = self.consume('ID').value
|
|
1777
1372
|
self.consume('IN')
|
|
1778
|
-
|
|
1779
|
-
# Check if it's range syntax: for i in range 1 10
|
|
1780
1373
|
if self.check('RANGE'):
|
|
1781
1374
|
self.consume('RANGE')
|
|
1782
1375
|
start_val = self.parse_expression()
|
|
1783
1376
|
end_val = self.parse_expression()
|
|
1784
|
-
|
|
1785
1377
|
self.consume('NEWLINE')
|
|
1786
1378
|
self.consume('INDENT')
|
|
1787
|
-
|
|
1788
1379
|
body = []
|
|
1789
1380
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1790
1381
|
while self.check('NEWLINE'): self.consume()
|
|
1791
1382
|
if self.check('DEDENT'): break
|
|
1792
1383
|
body.append(self.parse_statement())
|
|
1793
|
-
|
|
1794
1384
|
self.consume('DEDENT')
|
|
1795
|
-
|
|
1796
|
-
# Create ForIn with a range call
|
|
1797
1385
|
iterable = Call('range', [start_val, end_val])
|
|
1798
1386
|
node = ForIn(var_name, iterable, body)
|
|
1799
1387
|
node.line = start_token.line
|
|
1800
1388
|
return node
|
|
1801
1389
|
else:
|
|
1802
|
-
# for x in iterable
|
|
1803
1390
|
iterable = self.parse_expression()
|
|
1804
|
-
|
|
1805
1391
|
self.consume('NEWLINE')
|
|
1806
1392
|
self.consume('INDENT')
|
|
1807
|
-
|
|
1808
1393
|
body = []
|
|
1809
1394
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1810
1395
|
while self.check('NEWLINE'): self.consume()
|
|
1811
1396
|
if self.check('DEDENT'): break
|
|
1812
1397
|
body.append(self.parse_statement())
|
|
1813
|
-
|
|
1814
1398
|
self.consume('DEDENT')
|
|
1815
1399
|
node = ForIn(var_name, iterable, body)
|
|
1816
1400
|
node.line = start_token.line
|
|
1817
1401
|
return node
|
|
1818
1402
|
else:
|
|
1819
|
-
# Old style: for 20 in range (count-based)
|
|
1820
1403
|
count_expr = self.parse_expression()
|
|
1821
1404
|
self.consume('IN')
|
|
1822
1405
|
self.consume('RANGE')
|
|
1823
|
-
|
|
1824
1406
|
self.consume('NEWLINE')
|
|
1825
1407
|
self.consume('INDENT')
|
|
1826
|
-
|
|
1827
1408
|
body = []
|
|
1828
1409
|
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1829
1410
|
while self.check('NEWLINE'): self.consume()
|
|
1830
1411
|
if self.check('DEDENT'): break
|
|
1831
1412
|
body.append(self.parse_statement())
|
|
1832
|
-
|
|
1833
1413
|
self.consume('DEDENT')
|
|
1834
1414
|
node = For(count_expr, body)
|
|
1835
1415
|
node.line = start_token.line
|
|
1836
1416
|
return node
|
|
1837
|
-
|
|
1838
1417
|
def parse_expression_stmt(self) -> Node:
|
|
1839
|
-
# Implicit print for top-level expressions
|
|
1840
1418
|
expr = self.parse_expression()
|
|
1841
1419
|
self.consume('NEWLINE')
|
|
1842
|
-
# Wrap in Print node for implicit output behavior
|
|
1843
1420
|
node = Print(expression=expr)
|
|
1844
1421
|
node.line = expr.line
|
|
1845
1422
|
return node
|
|
1846
|
-
|
|
1847
1423
|
def parse_expression(self) -> Node:
|
|
1848
|
-
# Check for lambda: fn x => expr or fn x y => expr
|
|
1849
1424
|
if self.check('FN'):
|
|
1850
1425
|
return self.parse_lambda()
|
|
1851
|
-
|
|
1852
1426
|
return self.parse_ternary()
|
|
1853
|
-
|
|
1854
1427
|
def parse_lambda(self) -> Lambda:
|
|
1855
1428
|
token = self.consume('FN')
|
|
1856
1429
|
params = []
|
|
1857
|
-
|
|
1858
|
-
# Parse parameters until =>
|
|
1859
1430
|
while self.check('ID'):
|
|
1860
1431
|
params.append(self.consume('ID').value)
|
|
1861
|
-
|
|
1862
1432
|
self.consume('ARROW')
|
|
1863
1433
|
body = self.parse_expression()
|
|
1864
|
-
|
|
1865
1434
|
node = Lambda(params, body)
|
|
1866
1435
|
node.line = token.line
|
|
1867
1436
|
return node
|
|
1868
|
-
|
|
1869
1437
|
def parse_ternary(self) -> Node:
|
|
1870
|
-
# condition ? true_expr : false_expr
|
|
1871
1438
|
condition = self.parse_logic_or()
|
|
1872
|
-
|
|
1873
1439
|
if self.check('QUESTION'):
|
|
1874
1440
|
self.consume('QUESTION')
|
|
1875
1441
|
true_expr = self.parse_expression()
|
|
@@ -1878,107 +1444,75 @@ class Parser:
|
|
|
1878
1444
|
node = Ternary(condition, true_expr, false_expr)
|
|
1879
1445
|
node.line = condition.line
|
|
1880
1446
|
return node
|
|
1881
|
-
|
|
1882
1447
|
return condition
|
|
1883
|
-
|
|
1884
1448
|
def parse_logic_or(self) -> Node:
|
|
1885
1449
|
left = self.parse_logic_and()
|
|
1886
|
-
|
|
1887
1450
|
while self.check('OR'):
|
|
1888
1451
|
op_token = self.consume()
|
|
1889
1452
|
right = self.parse_logic_and()
|
|
1890
1453
|
new_node = BinOp(left, op_token.value, right)
|
|
1891
1454
|
new_node.line = op_token.line
|
|
1892
1455
|
left = new_node
|
|
1893
|
-
|
|
1894
1456
|
return left
|
|
1895
|
-
|
|
1896
1457
|
def parse_logic_and(self) -> Node:
|
|
1897
1458
|
left = self.parse_comparison()
|
|
1898
|
-
|
|
1899
1459
|
while self.check('AND'):
|
|
1900
1460
|
op_token = self.consume()
|
|
1901
1461
|
right = self.parse_comparison()
|
|
1902
1462
|
new_node = BinOp(left, op_token.value, right)
|
|
1903
1463
|
new_node.line = op_token.line
|
|
1904
1464
|
left = new_node
|
|
1905
|
-
|
|
1906
1465
|
return left
|
|
1907
|
-
|
|
1908
1466
|
def parse_comparison(self) -> Node:
|
|
1909
|
-
# Simple binary operators handling
|
|
1910
|
-
# precedence: ==, !=, <, >, <=, >=, is, matches
|
|
1911
1467
|
left = self.parse_arithmetic()
|
|
1912
|
-
|
|
1913
1468
|
if self.peek().type in ('EQ', 'NEQ', 'GT', 'LT', 'GE', 'LE', 'IS', 'MATCHES'):
|
|
1914
1469
|
op_token = self.consume()
|
|
1915
1470
|
op_val = op_token.value
|
|
1916
1471
|
if op_token.type == 'IS':
|
|
1917
|
-
op_val = '=='
|
|
1918
|
-
# matches is kept as matches
|
|
1919
|
-
|
|
1472
|
+
op_val = '=='
|
|
1920
1473
|
right = self.parse_arithmetic()
|
|
1921
1474
|
node = BinOp(left, op_val, right)
|
|
1922
1475
|
node.line = op_token.line
|
|
1923
1476
|
return node
|
|
1924
|
-
|
|
1925
1477
|
return left
|
|
1926
|
-
|
|
1927
1478
|
def parse_arithmetic(self) -> Node:
|
|
1928
|
-
# precedence: +, -
|
|
1929
1479
|
left = self.parse_term()
|
|
1930
|
-
|
|
1931
1480
|
while self.peek().type in ('PLUS', 'MINUS'):
|
|
1932
1481
|
op_token = self.consume()
|
|
1933
1482
|
right = self.parse_term()
|
|
1934
1483
|
new_node = BinOp(left, op_token.value, right)
|
|
1935
1484
|
new_node.line = op_token.line
|
|
1936
1485
|
left = new_node
|
|
1937
|
-
|
|
1938
1486
|
return left
|
|
1939
|
-
|
|
1940
1487
|
def parse_term(self) -> Node:
|
|
1941
|
-
# precedence: *, /, %
|
|
1942
1488
|
left = self.parse_factor()
|
|
1943
|
-
|
|
1944
1489
|
while self.peek().type in ('MUL', 'DIV', 'MOD'):
|
|
1945
1490
|
op_token = self.consume()
|
|
1946
1491
|
right = self.parse_factor()
|
|
1947
1492
|
new_node = BinOp(left, op_token.value, right)
|
|
1948
1493
|
new_node.line = op_token.line
|
|
1949
1494
|
left = new_node
|
|
1950
|
-
|
|
1951
1495
|
return left
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
1496
|
def parse_convert(self) -> Convert:
|
|
1955
|
-
"""Parse: convert expr to json"""
|
|
1956
1497
|
token = self.consume('CONVERT')
|
|
1957
|
-
expr = self.parse_factor()
|
|
1958
|
-
|
|
1498
|
+
expr = self.parse_factor()
|
|
1959
1499
|
self.consume('TO')
|
|
1960
|
-
|
|
1961
1500
|
target_format = 'json'
|
|
1962
1501
|
if self.check('JSON'):
|
|
1963
1502
|
self.consume('JSON')
|
|
1964
1503
|
elif self.check('ID'):
|
|
1965
1504
|
target_format = self.consume('ID').value
|
|
1966
|
-
|
|
1967
1505
|
node = Convert(expr, target_format)
|
|
1968
1506
|
node.line = token.line
|
|
1969
1507
|
return node
|
|
1970
|
-
|
|
1971
1508
|
def parse_download(self) -> Download:
|
|
1972
|
-
"""Parse: download 'url'"""
|
|
1973
1509
|
token = self.consume('DOWNLOAD')
|
|
1974
1510
|
url = self.parse_expression()
|
|
1975
1511
|
self.consume('NEWLINE')
|
|
1976
1512
|
node = Download(url)
|
|
1977
1513
|
node.line = token.line
|
|
1978
1514
|
return node
|
|
1979
|
-
|
|
1980
1515
|
def parse_archive(self) -> ArchiveOp:
|
|
1981
|
-
"""Parse: compress folder 'x' to 'y' / extract 'x' to 'y'"""
|
|
1982
1516
|
op = None
|
|
1983
1517
|
token = None
|
|
1984
1518
|
if self.check('COMPRESS'):
|
|
@@ -1988,37 +1522,22 @@ class Parser:
|
|
|
1988
1522
|
else:
|
|
1989
1523
|
token = self.consume('EXTRACT')
|
|
1990
1524
|
op = 'extract'
|
|
1991
|
-
|
|
1992
1525
|
source = self.parse_expression()
|
|
1993
1526
|
self.consume('TO')
|
|
1994
1527
|
target = self.parse_expression()
|
|
1995
1528
|
self.consume('NEWLINE')
|
|
1996
|
-
|
|
1997
1529
|
node = ArchiveOp(op, source, target)
|
|
1998
1530
|
node.line = token.line
|
|
1999
1531
|
return node
|
|
2000
|
-
|
|
2001
1532
|
def parse_csv_load(self) -> CsvOp:
|
|
2002
|
-
"""Parse: load csv 'path'"""
|
|
2003
1533
|
token = self.consume('LOAD')
|
|
2004
1534
|
self.consume('CSV')
|
|
2005
1535
|
path = self.parse_expression()
|
|
2006
|
-
# Should we allow assignment here? usually 'users = load csv ...'
|
|
2007
|
-
# Which is an Assign statement.
|
|
2008
|
-
# But parse_assign handles ID = ...
|
|
2009
|
-
# If we have 'users = load csv ...', parse_statement sees ID, goes to parse_id_start...
|
|
2010
|
-
# -> checks ASSIGN -> parse_expression.
|
|
2011
|
-
# So 'load csv' must be parsed as an EXPRESSION if used in assignment.
|
|
2012
|
-
# But here we added it to parse_statement.
|
|
2013
|
-
# If used as statement: `load csv "file"` -> implicitly prints result due to display logic?
|
|
2014
|
-
# We need to add `load` to parse_expression / parse_factor to be usable in assignment.
|
|
2015
1536
|
self.consume('NEWLINE')
|
|
2016
1537
|
node = CsvOp('load', None, path)
|
|
2017
1538
|
node.line = token.line
|
|
2018
1539
|
return node
|
|
2019
|
-
|
|
2020
1540
|
def parse_csv_save(self) -> CsvOp:
|
|
2021
|
-
"""Parse: save expr to csv 'path'"""
|
|
2022
1541
|
token = self.consume('SAVE')
|
|
2023
1542
|
data = self.parse_expression()
|
|
2024
1543
|
self.consume('TO')
|
|
@@ -2028,9 +1547,7 @@ class Parser:
|
|
|
2028
1547
|
node = CsvOp('save', data, path)
|
|
2029
1548
|
node.line = token.line
|
|
2030
1549
|
return node
|
|
2031
|
-
|
|
2032
1550
|
def parse_clipboard(self) -> Node:
|
|
2033
|
-
"""Parse: copy expr to clipboard OR paste from clipboard"""
|
|
2034
1551
|
if self.check('COPY'):
|
|
2035
1552
|
token = self.consume('COPY')
|
|
2036
1553
|
content = self.parse_expression()
|
|
@@ -2041,9 +1558,6 @@ class Parser:
|
|
|
2041
1558
|
node.line = token.line
|
|
2042
1559
|
return node
|
|
2043
1560
|
else:
|
|
2044
|
-
# Paste is usually an expression: text = paste from clipboard
|
|
2045
|
-
# If statement: paste from clipboard (useless unless implicit print?)
|
|
2046
|
-
# Let's support statement
|
|
2047
1561
|
token = self.consume('PASTE')
|
|
2048
1562
|
self.consume('FROM')
|
|
2049
1563
|
self.consume('CLIPBOARD')
|
|
@@ -2051,9 +1565,7 @@ class Parser:
|
|
|
2051
1565
|
node = ClipboardOp('paste', None)
|
|
2052
1566
|
node.line = token.line
|
|
2053
1567
|
return node
|
|
2054
|
-
|
|
2055
1568
|
def parse_automation(self) -> AutomationOp:
|
|
2056
|
-
"""Parse: press 'x', type 'x', click at x, y, notify 't' 'b'"""
|
|
2057
1569
|
if self.check('PRESS'):
|
|
2058
1570
|
token = self.consume('PRESS')
|
|
2059
1571
|
keys = self.parse_expression()
|
|
@@ -2068,7 +1580,7 @@ class Parser:
|
|
|
2068
1580
|
token = self.consume('CLICK')
|
|
2069
1581
|
self.consume('AT')
|
|
2070
1582
|
x = self.parse_expression()
|
|
2071
|
-
if self.check('COMMA'): self.consume('COMMA')
|
|
1583
|
+
if self.check('COMMA'): self.consume('COMMA')
|
|
2072
1584
|
y = self.parse_expression()
|
|
2073
1585
|
self.consume('NEWLINE')
|
|
2074
1586
|
return AutomationOp('click', [x, y])
|
|
@@ -2078,9 +1590,7 @@ class Parser:
|
|
|
2078
1590
|
msg = self.parse_expression()
|
|
2079
1591
|
self.consume('NEWLINE')
|
|
2080
1592
|
return AutomationOp('notify', [title, msg])
|
|
2081
|
-
|
|
2082
1593
|
def parse_write(self) -> FileWrite:
|
|
2083
|
-
"""Parse: write 'text' to file 'path'"""
|
|
2084
1594
|
token = self.consume('WRITE')
|
|
2085
1595
|
content = self.parse_expression()
|
|
2086
1596
|
self.consume('TO')
|
|
@@ -2090,9 +1600,7 @@ class Parser:
|
|
|
2090
1600
|
node = FileWrite(path, content, 'w')
|
|
2091
1601
|
node.line = token.line
|
|
2092
1602
|
return node
|
|
2093
|
-
|
|
2094
1603
|
def parse_append(self) -> FileWrite:
|
|
2095
|
-
"""Parse: append 'text' to file 'path'"""
|
|
2096
1604
|
token = self.consume('APPEND')
|
|
2097
1605
|
content = self.parse_expression()
|
|
2098
1606
|
self.consume('TO')
|
|
@@ -2102,4 +1610,3 @@ class Parser:
|
|
|
2102
1610
|
node = FileWrite(path, content, 'a')
|
|
2103
1611
|
node.line = token.line
|
|
2104
1612
|
return node
|
|
2105
|
-
|