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/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) # Hack: Wrap in Print to execute? No, just expression stmt
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() # We'll need to check "to csv" inside
137
- return self.parse_expression_stmt() # Fallback
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
- """Parse: a list of x, y, z"""
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
- """Parse: a unique set of x, y, z -> Set([x,y,z])"""
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') # break out if unexpected
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 # Trailing comma support
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() # argument
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
- # Treat as HTML tag function call
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() # argument
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 = '==' # Treat 'is' as equality
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() # parse simple factor or expression? 'convert data' - data is 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') # optional
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
-