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/interpreter.py CHANGED
@@ -26,181 +26,129 @@ import zipfile
26
26
  from datetime import timedelta
27
27
  import calendar
28
28
  import sqlite3
29
-
30
- # Optional dependencies handling
31
29
  try:
32
30
  import keyboard
33
31
  import mouse
34
32
  import pyperclip
35
33
  from plyer import notification
36
34
  except ImportError:
37
- pass # Will handle at runtime
38
-
39
-
40
-
35
+ pass
41
36
  class Environment:
42
37
  def __init__(self, parent=None):
43
38
  self.variables: Dict[str, Any] = {}
44
- self.constants: set = set() # Track constant names
39
+ self.constants: set = set()
45
40
  self.parent = parent
46
-
47
41
  def get(self, name: str) -> Any:
48
42
  if name in self.variables:
49
43
  return self.variables[name]
50
44
  if self.parent:
51
45
  return self.parent.get(name)
52
46
  raise NameError(f"Variable '{name}' is not defined.")
53
-
54
47
  def set(self, name: str, value: Any):
55
- # Check if it's a constant
56
48
  if name in self.constants:
57
49
  raise RuntimeError(f"Cannot reassign constant '{name}'")
58
50
  if self.parent and name in self.parent.constants:
59
51
  raise RuntimeError(f"Cannot reassign constant '{name}'")
60
52
  self.variables[name] = value
61
-
62
53
  def set_const(self, name: str, value: Any):
63
54
  if name in self.variables:
64
55
  raise RuntimeError(f"Constant '{name}' already declared")
65
56
  self.variables[name] = value
66
57
  self.constants.add(name)
67
-
68
58
  class ReturnException(Exception):
69
59
  def __init__(self, value):
70
60
  self.value = value
71
-
72
61
  class StopException(Exception):
73
- """Raised by 'stop' to break out of loops"""
74
62
  pass
75
-
76
63
  class SkipException(Exception):
77
- """Raised by 'skip' to continue to next iteration"""
78
64
  pass
79
-
80
65
  class ShellLiteError(Exception):
81
- """Custom error raised by 'error' statement"""
82
66
  def __init__(self, message):
83
67
  self.message = message
84
68
  super().__init__(message)
85
-
86
69
  class LambdaFunction:
87
- """Wrapper for lambda functions"""
88
70
  def __init__(self, params: List[str], body, interpreter):
89
71
  self.params = params
90
72
  self.body = body
91
73
  self.interpreter = interpreter
92
74
  self.closure_env = interpreter.current_env
93
-
94
75
  def __call__(self, *args):
95
76
  if len(args) != len(self.params):
96
77
  raise TypeError(f"Lambda expects {len(self.params)} args, got {len(args)}")
97
-
98
78
  old_env = self.interpreter.current_env
99
79
  new_env = Environment(parent=self.closure_env)
100
-
101
80
  for param, arg in zip(self.params, args):
102
81
  new_env.set(param, arg)
103
-
104
82
  self.interpreter.current_env = new_env
105
83
  try:
106
84
  result = self.interpreter.visit(self.body)
107
85
  finally:
108
86
  self.interpreter.current_env = old_env
109
-
110
87
  return result
111
-
112
88
  class Instance:
113
89
  def __init__(self, class_def: ClassDef):
114
90
  self.class_def = class_def
115
91
  self.data: Dict[str, Any] = {}
116
-
117
92
  class Tag:
118
- """HTML Tag Builder"""
119
93
  def __init__(self, name: str, attrs: Dict[str, Any] = None):
120
94
  self.name = name
121
95
  self.attrs = attrs or {}
122
96
  self.children: List[Any] = []
123
-
124
97
  def add(self, child):
125
- # Prevent double-addition (once by push, once by return value capture)
126
- # Equality check might be expensive if deep, use identity for objects?
127
- # But 'child' can be string.
128
- # String duplicates are allowed.
129
- # Tag duplicates (identity) are not.
130
98
  if isinstance(child, Tag):
131
99
  if any(c is child for c in self.children):
132
100
  return
133
101
  self.children.append(child)
134
-
135
102
  def __str__(self):
136
- # Render attributes
137
103
  attr_str = ""
138
104
  for k, v in self.attrs.items():
139
105
  attr_str += f' {k}="{v}"'
140
-
141
- # Render children
142
106
  inner = ""
143
107
  for child in self.children:
144
108
  inner += str(child)
145
-
146
- # Void tags?
147
109
  if self.name in ('img', 'br', 'hr', 'input', 'meta', 'link'):
148
110
  return f"<{self.name}{attr_str} />"
149
-
150
111
  return f"<{self.name}{attr_str}>{inner}</{self.name}>"
151
-
152
112
  class WebBuilder:
153
- """Context manager for nested tags"""
154
113
  def __init__(self, interpreter):
155
114
  self.stack: List[Tag] = []
156
115
  self.interpreter = interpreter
157
-
158
116
  def push(self, tag: Tag):
159
117
  if self.stack:
160
118
  self.stack[-1].add(tag)
161
119
  self.stack.append(tag)
162
-
163
120
  def pop(self):
164
121
  if not self.stack: return None
165
122
  return self.stack.pop()
166
-
167
123
  def add_text(self, text: str):
168
124
  if self.stack:
169
125
  self.stack[-1].add(text)
170
126
  else:
171
- # If no root tag, maybe we just print? No, assume returning string
172
127
  pass
173
-
174
128
  class Interpreter:
175
129
  def __init__(self):
176
130
  self.global_env = Environment()
177
131
  self.current_env = self.global_env
178
132
  self.functions: Dict[str, FunctionDef] = {}
179
133
  self.classes: Dict[str, ClassDef] = {}
180
- self.http_routes = [] # List of (pattern_str, compiled_regex, body_node)
134
+ self.http_routes = []
181
135
  self.middleware_routes = []
182
- self.static_routes = {} # url_prefix -> folder_path
136
+ self.static_routes = {}
183
137
  self.web = WebBuilder(self)
184
138
  self.db_conn = None
185
-
186
-
187
- # Built-in functions
188
139
  self.builtins = {
189
140
  'str': str, 'int': int, 'float': float, 'bool': bool,
190
141
  'list': list, 'len': len,
191
142
  'range': lambda *args: list(range(*args)),
192
143
  'typeof': lambda x: type(x).__name__,
193
-
194
144
  'run': self.builtin_run,
195
145
  'read': self.builtin_read,
196
146
  'write': self.builtin_write,
197
147
  'json_parse': self.builtin_json_parse,
198
148
  'json_stringify': self.builtin_json_stringify,
199
149
  'print': print,
200
-
201
150
  'abs': abs, 'min': min, 'max': max,
202
151
  'round': round, 'pow': pow, 'sum': sum,
203
-
204
152
  'split': lambda s, d=" ": s.split(d),
205
153
  'join': lambda lst, d="": d.join(str(x) for x in lst),
206
154
  'replace': lambda s, old, new: s.replace(old, new),
@@ -211,10 +159,9 @@ class Interpreter:
211
159
  'endswith': lambda s, p: s.endswith(p),
212
160
  'find': lambda s, sub: s.find(sub),
213
161
  'char': chr, 'ord': ord,
214
-
215
162
  'append': lambda l, x: (l.append(x), l)[1],
216
163
  'push': self._builtin_push,
217
- 'count': len, # Natural alias: count of tasks
164
+ 'count': len,
218
165
  'remove': lambda l, x: l.remove(x),
219
166
  'pop': lambda l, idx=-1: l.pop(idx),
220
167
  'get': lambda l, idx: l[idx],
@@ -224,21 +171,17 @@ class Interpreter:
224
171
  'slice': lambda l, start, end=None: l[start:end],
225
172
  'contains': lambda l, x: x in l,
226
173
  'index': lambda l, x: l.index(x) if x in l else -1,
227
-
228
174
  'map': self._builtin_map,
229
175
  'filter': self._builtin_filter,
230
176
  'reduce': self._builtin_reduce,
231
-
232
177
  'exists': os.path.exists,
233
178
  'delete': os.remove,
234
179
  'copy': shutil.copy,
235
180
  'rename': os.rename,
236
181
  'mkdir': lambda p: os.makedirs(p, exist_ok=True),
237
182
  'listdir': os.listdir,
238
-
239
183
  'http_get': self.builtin_http_get,
240
184
  'http_post': self.builtin_http_post,
241
-
242
185
  'random': random.random,
243
186
  'randint': random.randint,
244
187
  'sleep': time.sleep,
@@ -255,14 +198,11 @@ class Interpreter:
255
198
  'wait': time.sleep,
256
199
  'push': self._builtin_push,
257
200
  'remove': lambda lst, item: lst.remove(item),
258
- 'Set': set, # Feature 6: Sets
201
+ 'Set': set,
259
202
  'show': print,
260
203
  'say': print,
261
204
  'today': lambda: datetime.now().strftime("%Y-%m-%d"),
262
205
  }
263
-
264
- # Add basic HTML tags
265
- # Web DSL Tags
266
206
  tags = [
267
207
  'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
268
208
  'span', 'a', 'img', 'button', 'input', 'form',
@@ -275,89 +215,53 @@ class Interpreter:
275
215
  ]
276
216
  for t in tags:
277
217
  self.builtins[t] = self._make_tag_fn(t)
278
-
279
- # System Builtins
280
218
  self.builtins['env'] = lambda name: os.environ.get(str(name), None)
281
219
  self.builtins['int'] = lambda x: int(float(x)) if x else 0
282
220
  self.builtins['str'] = lambda x: str(x)
283
-
284
221
  class TimeWrapper:
285
222
  def now(self):
286
223
  return str(int(time.time()))
287
224
  self.builtins['time'] = TimeWrapper()
288
-
289
-
290
225
  self._init_std_modules()
291
-
292
- # Register builtins in global environment
293
226
  for k, v in self.builtins.items():
294
227
  self.global_env.set(k, v)
295
-
296
228
  def _make_tag_fn(self, tag_name):
297
- # Returns a callable that creates a Tag
298
229
  def tag_fn(*args):
299
- # Parse args:
300
- # 1. Strings are text content (optional, mostly for single line)
301
- # 2. Assign/PropAssign? No, standard args here.
302
- # ShellLite doesn't have keyword args in Call node generically yet for builtins.
303
- # But we often pass "class='foo'". This comes as a String "class=foo" if user types it?
304
- # Or dictionary?
305
- # Convention:
306
- # - Strings with '=' are attributes
307
- # - Strings without '=' are text content
308
- # - Dicts are attributes
309
-
310
230
  attrs = {}
311
231
  content = []
312
-
313
232
  for arg in args:
314
233
  if isinstance(arg, dict):
315
234
  attrs.update(arg)
316
235
  elif isinstance(arg, str):
317
236
  if '=' in arg and not ' ' in arg and arg.split('=')[0].isalnum():
318
- # Simple attribute parsing: id=main
319
237
  k, v = arg.split('=', 1)
320
238
  attrs[k] = v
321
239
  else:
322
240
  content.append(arg)
323
241
  else:
324
242
  content.append(str(arg))
325
-
326
243
  t = Tag(tag_name, attrs)
327
244
  for c in content:
328
245
  t.add(c)
329
-
330
- # If we are in a web stack, this is already added by push?
331
- # But wait, visit_Call will handle the BLOCK by passing context.
332
- # This function just creates the Node.
333
- # The 'block' handling must be in visit_Call.
334
246
  return t
335
247
  return tag_fn
336
-
337
248
  def _builtin_map(self, lst, func):
338
- """Map function over list"""
339
249
  if callable(func):
340
250
  return [func(x) for x in lst]
341
251
  raise TypeError("map requires a callable")
342
-
343
252
  def _builtin_filter(self, lst, func):
344
- """Filter list by predicate"""
345
253
  if callable(func):
346
254
  return [x for x in lst if func(x)]
347
255
  raise TypeError("filter requires a callable")
348
-
349
256
  def _builtin_reduce(self, lst, func, initial=None):
350
- """Reduce list with function"""
351
257
  if callable(func):
352
258
  if initial is not None:
353
259
  return functools.reduce(func, lst, initial)
354
260
  return functools.reduce(func, lst)
355
261
  raise TypeError("reduce requires a callable")
356
-
357
262
  def _builtin_push(self, lst, item):
358
263
  lst.append(item)
359
264
  return None
360
-
361
265
  def _init_std_modules(self):
362
266
  self.std_modules = {
363
267
  'math': {
@@ -433,21 +337,17 @@ class Interpreter:
433
337
  'split': lambda p, s: re.split(p, s),
434
338
  },
435
339
  }
436
-
437
340
  def _http_get(self, url):
438
341
  with urllib.request.urlopen(url) as response:
439
342
  return response.read().decode('utf-8')
440
-
441
343
  def _http_post(self, url, data):
442
344
  if isinstance(data, str):
443
345
  json_data = data.encode('utf-8')
444
346
  else:
445
347
  json_data = json.dumps(data).encode('utf-8')
446
-
447
348
  req = urllib.request.Request(url, data=json_data, headers={'Content-Type': 'application/json'})
448
349
  with urllib.request.urlopen(req) as response:
449
350
  return response.read().decode('utf-8')
450
-
451
351
  def visit(self, node: Node) -> Any:
452
352
  try:
453
353
  method_name = f'visit_{type(node).__name__}'
@@ -459,19 +359,14 @@ class Interpreter:
459
359
  if not hasattr(e, 'line') and hasattr(node, 'line'):
460
360
  e.line = node.line
461
361
  raise e
462
-
463
362
  def generic_visit(self, node: Node):
464
363
  raise Exception(f'No visit_{type(node).__name__} method')
465
-
466
364
  def visit_Number(self, node: Number):
467
365
  return node.value
468
-
469
366
  def visit_String(self, node: String):
470
367
  return node.value
471
-
472
368
  def visit_Boolean(self, node: Boolean):
473
369
  return node.value
474
-
475
370
  def visit_ListVal(self, node: ListVal):
476
371
  result = []
477
372
  for e in node.elements:
@@ -483,14 +378,11 @@ class Interpreter:
483
378
  else:
484
379
  result.append(self.visit(e))
485
380
  return result
486
-
487
381
  def visit_Dictionary(self, node: Dictionary):
488
382
  return {self.visit(k): self.visit(v) for k, v in node.pairs}
489
-
490
383
  def visit_PropertyAssign(self, node: PropertyAssign):
491
384
  instance = self.current_env.get(node.instance_name)
492
385
  val = self.visit(node.value)
493
-
494
386
  if isinstance(instance, Instance):
495
387
  instance.data[node.property_name] = val
496
388
  return val
@@ -499,34 +391,25 @@ class Interpreter:
499
391
  return val
500
392
  else:
501
393
  raise TypeError(f"Cannot assign property '{node.property_name}' of non-object '{node.instance_name}'")
502
-
503
394
  def visit_VarAccess(self, node: VarAccess):
504
395
  try:
505
396
  return self.current_env.get(node.name)
506
397
  except NameError:
507
398
  if node.name in self.builtins:
508
399
  val = self.builtins[node.name]
509
- # Auto-call 0-arg getters for convenience
510
400
  if node.name in ('random', 'time_now', 'date_str'):
511
401
  return val()
512
402
  return val
513
-
514
- # Check if it's a known function (Auto-call 0-arg / default arg functions)
515
403
  if node.name in self.functions:
516
- # Delegate to visit_Call with empty arguments
517
404
  return self.visit_Call(Call(node.name, []))
518
-
519
405
  raise
520
-
521
406
  def visit_Assign(self, node: Assign):
522
407
  value = self.visit(node.value)
523
408
  self.current_env.set(node.name, value)
524
409
  return value
525
-
526
410
  def visit_BinOp(self, node: BinOp):
527
411
  left = self.visit(node.left)
528
412
  right = self.visit(node.right)
529
-
530
413
  if node.op == '+':
531
414
  if isinstance(left, str) or isinstance(right, str):
532
415
  return str(left) + str(right)
@@ -563,13 +446,9 @@ class Interpreter:
563
446
  return bool(pattern.search(str(left)))
564
447
  return bool(re.search(str(pattern), str(left)))
565
448
  raise Exception(f"Unknown operator: {node.op}")
566
-
567
449
  def visit_Print(self, node: Print):
568
450
  value = self.visit(node.expression)
569
-
570
- # Apply colors/styles
571
451
  if node.color or node.style:
572
- # Color map
573
452
  colors = {
574
453
  'red': '91', 'green': '92', 'yellow': '93', 'blue': '94',
575
454
  'magenta': '95', 'cyan': '96'
@@ -577,18 +456,14 @@ class Interpreter:
577
456
  code_parts = []
578
457
  if node.style == 'bold':
579
458
  code_parts.append('1')
580
-
581
459
  if node.color and node.color.lower() in colors:
582
460
  code_parts.append(colors[node.color.lower()])
583
-
584
461
  if code_parts:
585
462
  ansi_code = "\033[" + ";".join(code_parts) + "m"
586
463
  print(f"{ansi_code}{value}\033[0m")
587
464
  return value
588
-
589
465
  print(value)
590
466
  return value
591
-
592
467
  def visit_If(self, node: If):
593
468
  condition = self.visit(node.condition)
594
469
  if condition:
@@ -597,12 +472,10 @@ class Interpreter:
597
472
  elif node.else_body:
598
473
  for stmt in node.else_body:
599
474
  self.visit(stmt)
600
-
601
475
  def visit_For(self, node: For):
602
476
  count = self.visit(node.count)
603
477
  if not isinstance(count, int):
604
478
  raise TypeError(f"Loop count must be an integer, got {type(count)}")
605
-
606
479
  for _ in range(count):
607
480
  try:
608
481
  for stmt in node.body:
@@ -613,12 +486,10 @@ class Interpreter:
613
486
  continue
614
487
  except ReturnException:
615
488
  raise
616
-
617
489
  def visit_Input(self, node: Input):
618
490
  if node.prompt:
619
491
  return input(node.prompt)
620
492
  return input()
621
-
622
493
  def visit_While(self, node: While):
623
494
  while self.visit(node.condition):
624
495
  try:
@@ -630,7 +501,6 @@ class Interpreter:
630
501
  continue
631
502
  except ReturnException:
632
503
  raise
633
-
634
504
  def visit_Try(self, node: Try):
635
505
  try:
636
506
  for stmt in node.try_body:
@@ -642,9 +512,7 @@ class Interpreter:
642
512
  self.current_env.set(node.catch_var, error_msg)
643
513
  for stmt in node.catch_body:
644
514
  self.visit(stmt)
645
-
646
515
  def visit_TryAlways(self, node: TryAlways):
647
- """Try with always (finally) block"""
648
516
  try:
649
517
  try:
650
518
  for stmt in node.try_body:
@@ -659,50 +527,33 @@ class Interpreter:
659
527
  finally:
660
528
  for stmt in node.always_body:
661
529
  self.visit(stmt)
662
-
663
530
  def visit_UnaryOp(self, node: UnaryOp):
664
531
  val = self.visit(node.right)
665
532
  if node.op == 'not':
666
533
  return not val
667
534
  raise Exception(f"Unknown unary operator: {node.op}")
668
-
669
535
  def visit_FunctionDef(self, node: FunctionDef):
670
- # Store function definition
671
536
  self.functions[node.name] = node
672
-
673
537
  def visit_Return(self, node: Return):
674
538
  value = self.visit(node.value)
675
539
  raise ReturnException(value)
676
-
677
540
  def _call_function_def(self, func_def: FunctionDef, args: List[Node]):
678
- # 1. Check if too many args provided
679
541
  if len(args) > len(func_def.args):
680
542
  raise TypeError(f"Function '{func_def.name}' expects max {len(func_def.args)} arguments, got {len(args)}")
681
-
682
- # Create new scope
683
543
  old_env = self.current_env
684
544
  new_env = Environment(parent=self.global_env)
685
-
686
545
  for i, (arg_name, default_node, type_hint) in enumerate(func_def.args):
687
546
  if i < len(args):
688
- # Use provided argument
689
547
  val = self.visit(args[i])
690
548
  elif default_node is not None:
691
- # Use default value
692
549
  val = self.visit(default_node)
693
550
  else:
694
551
  raise TypeError(f"Missing required argument '{arg_name}' for function '{func_def.name}'")
695
-
696
- # --- Type Checking ---
697
552
  if type_hint:
698
553
  self._check_type(arg_name, val, type_hint)
699
-
700
554
  new_env.set(arg_name, val)
701
-
702
555
  self.current_env = new_env
703
-
704
556
  ret_val = None
705
-
706
557
  try:
707
558
  for stmt in func_def.body:
708
559
  val = self.visit(stmt)
@@ -711,64 +562,34 @@ class Interpreter:
711
562
  ret_val = e.value
712
563
  finally:
713
564
  self.current_env = old_env
714
-
715
565
  return ret_val
716
-
717
-
718
566
  def visit_Call(self, node: Call):
719
- # Check built-ins first
720
567
  if node.name in self.builtins:
721
568
  args = [self.visit(a) for a in node.args]
722
569
  result = self.builtins[node.name](*args)
723
-
724
- # If this is a Tag and we have a block body
725
570
  if isinstance(result, Tag):
726
571
  if node.body:
727
572
  self.web.push(result)
728
573
  try:
729
574
  for stmt in node.body:
730
575
  res = self.visit(stmt)
731
- # If stmt expression returns a string/Tag, add it?
732
- # In ShellLite, statements don't naturally return unless expression stmt.
733
- # visit(Expression) returns value.
734
- # We should auto-add expression results to current tag?
735
576
  if res is not None and (isinstance(res, str) or isinstance(res, Tag)):
736
577
  self.web.add_text(res)
737
578
  finally:
738
579
  self.web.pop()
739
- # If it's a top-level tag in a route, we probably want to return it?
740
- # But 'result' is the tag itself.
741
- # If we pushed it, we populated it.
742
580
  return result
743
-
744
581
  return result
745
-
746
- # Check for lambda/callable in environment
747
- # Check for lambda/callable/object in environment
748
582
  try:
749
583
  func = self.current_env.get(node.name)
750
584
  if callable(func):
751
585
  args = [self.visit(a) for a in node.args]
752
586
  return func(*args)
753
-
754
- # Feature: Implicit Indexing via [...] syntax
755
- # If obj is List/Dict/Str/Instance.
756
- # parsed: name [i] [j] -> Call(name, [ListVal([i]), ListVal([j])])
757
-
758
587
  curr_obj = func
759
588
  if (isinstance(curr_obj, (list, dict, str)) or isinstance(curr_obj, Instance)):
760
- # Try to apply indexing for ALL args sequentially
761
- # Verify ALL args are ListVal-like (wrapped in list of 1)
762
-
763
- # We interpret this as chained access ONLY if object is not callable
764
- # (Lists/Dicts are not callable, so safe)
765
-
766
- # Check args
767
589
  valid_chain = True
768
590
  for arg_node in node.args:
769
591
  val = self.visit(arg_node)
770
592
  if isinstance(val, list) and len(val) == 1:
771
- # It's an index wrapper
772
593
  idx = val[0]
773
594
  try:
774
595
  curr_obj = curr_obj[idx]
@@ -779,61 +600,38 @@ class Interpreter:
779
600
  else:
780
601
  valid_chain = False
781
602
  break
782
-
783
603
  if valid_chain:
784
604
  return curr_obj
785
-
786
- # If invalid chain, we fall through to "pass" -> NameError?
787
- # Or maybe user tried to call a List with parens? list(1)?
788
- # We can error here explicitly if we want.
789
605
  pass
790
-
791
606
  except NameError:
792
607
  pass
793
-
794
608
  except NameError:
795
609
  pass
796
-
797
610
  if node.name not in self.functions:
798
611
  raise NameError(f"Function '{node.name}' not defined (and not a variable).")
799
-
800
612
  func_def = self.functions[node.name]
801
613
  return self._call_function_def(func_def, node.args)
802
-
803
-
804
614
  def visit_ClassDef(self, node: ClassDef):
805
615
  self.classes[node.name] = node
806
-
807
616
  def visit_Instantiation(self, node: Instantiation):
808
617
  if node.class_name not in self.classes:
809
618
  raise NameError(f"Class '{node.class_name}' not defined.")
810
-
811
619
  class_def = self.classes[node.class_name]
812
-
813
- # Get all properties including inherited
814
620
  all_properties = self._get_class_properties(class_def)
815
-
816
621
  if len(node.args) != len(all_properties):
817
622
  raise TypeError(f"Structure '{node.class_name}' expects {len(all_properties)} args for properties {all_properties}, got {len(node.args)}")
818
-
819
623
  instance = Instance(class_def)
820
624
  for prop, arg_expr in zip(all_properties, node.args):
821
625
  val = self.visit(arg_expr)
822
626
  instance.data[prop] = val
823
-
824
- # Store instance in variable
825
627
  self.current_env.set(node.var_name, instance)
826
628
  return instance
827
-
828
629
  def visit_MethodCall(self, node: MethodCall):
829
630
  instance = self.current_env.get(node.instance_name)
830
-
831
- # Support native modules (dicts of callables)
832
631
  if isinstance(instance, dict):
833
632
  if node.method_name not in instance:
834
633
  raise AttributeError(f"Module '{node.instance_name}' has no method '{node.method_name}'")
835
634
  method = instance[node.method_name]
836
-
837
635
  if isinstance(method, FunctionDef):
838
636
  return self._call_function_def(method, node.args)
839
637
  elif callable(method):
@@ -842,9 +640,6 @@ class Interpreter:
842
640
  return method(*args)
843
641
  except Exception as e:
844
642
  raise RuntimeError(f"Error calling '{node.instance_name}.{node.method_name}': {e}")
845
-
846
- # Support Implicit Indexing: request.form['key']
847
- # If property is list/dict/str and args are indices
848
643
  elif isinstance(method, (dict, list, str)):
849
644
  curr_obj = method
850
645
  valid_chain = True
@@ -860,46 +655,26 @@ class Interpreter:
860
655
  valid_chain = False; break
861
656
  else:
862
657
  valid_chain = False; break
863
-
864
658
  if valid_chain:
865
659
  return curr_obj
866
-
867
- # If we are here, it wasn't a valid index chain OR callable
868
660
  raise TypeError(f"Property '{node.method_name}' is not callable and index access failed.")
869
661
  else:
870
662
  raise TypeError(f"Property '{node.method_name}' is not callable.")
871
-
872
-
873
-
874
-
875
- # Fallback: Support native Python methods on ANY object (e.g. Set.add, Str.upper)
876
663
  if hasattr(instance, node.method_name) and callable(getattr(instance, node.method_name)):
877
664
  method = getattr(instance, node.method_name)
878
665
  args = [self.visit(a) for a in node.args]
879
666
  return method(*args)
880
-
881
667
  if not isinstance(instance, Instance):
882
668
  raise TypeError(f"'{node.instance_name}' is not a structure instance (and has no native method '{node.method_name}').")
883
-
884
- # Find method in class (with inheritance)
885
669
  method_node = self._find_method(instance.class_def, node.method_name)
886
-
887
670
  if not method_node:
888
671
  raise AttributeError(f"Structure '{instance.class_def.name}' has no method '{node.method_name}'")
889
-
890
- # Execute method
891
- # Create scope
892
672
  old_env = self.current_env
893
673
  new_env = Environment(parent=self.global_env)
894
-
895
- # Bind properties to scope
896
674
  for k, v in instance.data.items():
897
675
  new_env.set(k, v)
898
-
899
- # Bind args with defaults support (for methods)
900
676
  if len(node.args) > len(method_node.args):
901
677
  raise TypeError(f"Method '{node.method_name}' expects max {len(method_node.args)} arguments.")
902
-
903
678
  for i, (arg_name, default_node) in enumerate(method_node.args):
904
679
  if i < len(node.args):
905
680
  val = self.visit(node.args[i])
@@ -907,11 +682,8 @@ class Interpreter:
907
682
  val = self.visit(default_node)
908
683
  else:
909
684
  raise TypeError(f"Missing required argument '{arg_name}' for method '{node.method_name}'")
910
-
911
685
  new_env.set(arg_name, val)
912
-
913
686
  self.current_env = new_env
914
-
915
687
  ret_val = None
916
688
  try:
917
689
  for stmt in method_node.body:
@@ -919,50 +691,35 @@ class Interpreter:
919
691
  except ReturnException as e:
920
692
  ret_val = e.value
921
693
  finally:
922
- # Sync properties back to instance data for state mutation support
923
694
  for k in instance.data.keys():
924
695
  if k in new_env.variables:
925
696
  instance.data[k] = new_env.variables[k]
926
697
  self.current_env = old_env
927
-
928
698
  return ret_val
929
-
930
699
  def visit_PropertyAccess(self, node: PropertyAccess):
931
700
  instance = self.current_env.get(node.instance_name)
932
-
933
701
  if isinstance(instance, Instance):
934
702
  if node.property_name not in instance.data:
935
703
  raise AttributeError(f"Structure '{instance.class_def.name}' has no property '{node.property_name}'")
936
704
  return instance.data[node.property_name]
937
705
  elif isinstance(instance, dict):
938
- # Support data.x for {"x": 1}
939
706
  if node.property_name in instance:
940
707
  return instance[node.property_name]
941
- # Also assume the user might have used keys that match property name
942
708
  raise AttributeError(f"Dictionary has no key '{node.property_name}'")
943
-
944
709
  raise TypeError(f"'{node.instance_name}' is not a structure instance or dictionary.")
945
-
946
710
  def visit_Import(self, node: Import):
947
711
  if node.path in self.std_modules:
948
- # Import standard module as a dictionary property
949
- # To support 'math.sin', we set 'math' variable to the dict
950
712
  self.current_env.set(node.path, self.std_modules[node.path])
951
713
  return
952
-
953
- # For file imports with alias
954
- # 1. Check Local File
955
- import os # Added import for os module
714
+ import os
956
715
  if os.path.exists(node.path):
957
716
  target_path = node.path
958
- # 2. Check Global Modules (~/.shell_lite/modules)
959
717
  else:
960
718
  home = os.path.expanduser("~")
961
719
  global_path = os.path.join(home, ".shell_lite", "modules", node.path)
962
720
  if os.path.exists(global_path):
963
721
  target_path = global_path
964
722
  else:
965
- # Implicit .shl extension check
966
723
  if not node.path.endswith('.shl'):
967
724
  global_path_ext = global_path + ".shl"
968
725
  if os.path.exists(global_path_ext):
@@ -971,81 +728,60 @@ class Interpreter:
971
728
  raise FileNotFoundError(f"Could not find imported file: {node.path} (searched local and global modules)")
972
729
  else:
973
730
  raise FileNotFoundError(f"Could not find imported file: {node.path} (searched local and global modules)")
974
-
975
- # Folder Support: If it's a directory, assume main.shl
976
731
  if os.path.isdir(target_path):
977
732
  main_shl = os.path.join(target_path, "main.shl")
978
733
  pkg_shl = os.path.join(target_path, f"{os.path.basename(target_path)}.shl")
979
-
980
734
  if os.path.exists(main_shl):
981
735
  target_path = main_shl
982
736
  elif os.path.exists(pkg_shl):
983
737
  target_path = pkg_shl
984
738
  else:
985
739
  raise FileNotFoundError(f"Package '{node.path}' is a folder but has no 'main.shl' or '{os.path.basename(target_path)}.shl'.")
986
-
987
740
  try:
988
741
  with open(target_path, 'r', encoding='utf-8') as f:
989
742
  code = f.read()
990
743
  except FileNotFoundError:
991
744
  raise FileNotFoundError(f"Could not find imported file: {node.path}")
992
-
993
745
  from .lexer import Lexer
994
746
  from .parser import Parser
995
-
996
747
  lexer = Lexer(code)
997
748
  tokens = lexer.tokenize()
998
749
  parser = Parser(tokens)
999
750
  statements = parser.parse()
1000
-
1001
751
  for stmt in statements:
1002
752
  self.visit(stmt)
1003
-
1004
- # --- Helper methods for Inheritance ---
1005
753
  def _get_class_properties(self, class_def: ClassDef) -> List[str]:
1006
754
  props = list(class_def.properties)
1007
755
  if class_def.parent:
1008
756
  if class_def.parent not in self.classes:
1009
757
  raise NameError(f"Parent class '{class_def.parent}' not defined.")
1010
758
  parent_def = self.classes[class_def.parent]
1011
- # Parent props come first? Or child? Usually parent first in args.
1012
759
  return self._get_class_properties(parent_def) + props
1013
760
  return props
1014
-
1015
761
  def _find_method(self, class_def: ClassDef, method_name: str) -> Optional[FunctionDef]:
1016
762
  for m in class_def.methods:
1017
763
  if m.name == method_name:
1018
764
  return m
1019
-
1020
765
  if class_def.parent:
1021
766
  if class_def.parent not in self.classes:
1022
767
  raise NameError(f"Parent class '{class_def.parent}' not defined.")
1023
768
  parent_def = self.classes[class_def.parent]
1024
769
  return self._find_method(parent_def, method_name)
1025
-
1026
770
  return None
1027
-
1028
-
1029
- # --- Built-in Implementations ---
1030
-
1031
771
  def builtin_run(self, cmd):
1032
- # Run shell command
1033
772
  try:
1034
- # shell=True allows full shell syntax
1035
773
  result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
1036
774
  if result.returncode != 0:
1037
775
  print(f"Command Error: {result.stderr}")
1038
776
  return result.stdout.strip()
1039
777
  except Exception as e:
1040
778
  raise RuntimeError(f"Failed to run command: {e}")
1041
-
1042
779
  def builtin_read(self, path):
1043
780
  try:
1044
781
  with open(path, 'r', encoding='utf-8') as f:
1045
782
  return f.read()
1046
783
  except Exception as e:
1047
784
  raise RuntimeError(f"Failed to read file '{path}': {e}")
1048
-
1049
785
  def builtin_write(self, path, content):
1050
786
  try:
1051
787
  with open(path, 'w', encoding='utf-8') as f:
@@ -1053,88 +789,62 @@ class Interpreter:
1053
789
  return True
1054
790
  except Exception as e:
1055
791
  raise RuntimeError(f"Failed to write file '{path}': {e}")
1056
-
1057
792
  def builtin_json_parse(self, json_str):
1058
793
  try:
1059
794
  return json.loads(json_str)
1060
795
  except Exception as e:
1061
796
  raise RuntimeError(f"Invalid JSON: {e}")
1062
-
1063
797
  def builtin_json_stringify(self, obj):
1064
798
  try:
1065
- # Convert internal Instance objects to dicts if possible?
1066
799
  if isinstance(obj, Instance):
1067
800
  return json.dumps(obj.data)
1068
801
  return json.dumps(obj)
1069
802
  except Exception as e:
1070
803
  raise RuntimeError(f"JSON stringify failed: {e}")
1071
-
1072
804
  def builtin_http_get(self, url):
1073
805
  try:
1074
806
  with urllib.request.urlopen(url) as response:
1075
807
  return response.read().decode('utf-8')
1076
808
  except Exception as e:
1077
809
  raise RuntimeError(f"HTTP GET failed for '{url}': {e}")
1078
-
1079
810
  def builtin_http_post(self, url, data_dict):
1080
811
  try:
1081
- # Ensure data_dict is a valid dictionary/object
1082
812
  if isinstance(data_dict, Instance):
1083
813
  data_dict = data_dict.data
1084
-
1085
814
  data = json.dumps(data_dict).encode('utf-8')
1086
815
  req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
1087
816
  with urllib.request.urlopen(req) as response:
1088
817
  return response.read().decode('utf-8')
1089
818
  except Exception as e:
1090
819
  raise RuntimeError(f"HTTP POST failed for '{url}': {e}")
1091
-
1092
- # --- New Feature Visitors ---
1093
-
1094
820
  def visit_Lambda(self, node: Lambda):
1095
- """Create a callable lambda function"""
1096
821
  return LambdaFunction(node.params, node.body, self)
1097
-
1098
822
  def visit_Ternary(self, node: Ternary):
1099
- """Evaluate ternary expression: condition ? true_expr : false_expr"""
1100
823
  condition = self.visit(node.condition)
1101
824
  if condition:
1102
825
  return self.visit(node.true_expr)
1103
826
  else:
1104
827
  return self.visit(node.false_expr)
1105
-
1106
828
  def visit_ListComprehension(self, node: ListComprehension):
1107
- """Evaluate list comprehension: [expr for var in iterable if condition]"""
1108
829
  iterable = self.visit(node.iterable)
1109
-
1110
830
  if not hasattr(iterable, '__iter__'):
1111
831
  raise TypeError(f"Cannot iterate over {type(iterable).__name__}")
1112
-
1113
832
  result = []
1114
833
  old_env = self.current_env
1115
834
  new_env = Environment(parent=self.current_env)
1116
835
  self.current_env = new_env
1117
-
1118
836
  try:
1119
837
  for item in iterable:
1120
838
  new_env.set(node.var_name, item)
1121
-
1122
- # Check condition if present
1123
839
  if node.condition:
1124
840
  if not self.visit(node.condition):
1125
841
  continue
1126
-
1127
842
  result.append(self.visit(node.expr))
1128
843
  finally:
1129
844
  self.current_env = old_env
1130
-
1131
845
  return result
1132
-
1133
846
  def visit_Spread(self, node: Spread):
1134
- """Spread operator - returns the value to be spread"""
1135
847
  return self.visit(node.value)
1136
-
1137
- # --- GUI Features ---
1138
848
  def visit_Alert(self, node: Alert):
1139
849
  msg = self.visit(node.message)
1140
850
  root = tk.Tk()
@@ -1142,7 +852,6 @@ class Interpreter:
1142
852
  root.attributes('-topmost', True)
1143
853
  messagebox.showinfo("Alert", str(msg))
1144
854
  root.destroy()
1145
-
1146
855
  def visit_Prompt(self, node: Prompt):
1147
856
  prompt = self.visit(node.prompt)
1148
857
  root = tk.Tk()
@@ -1151,7 +860,6 @@ class Interpreter:
1151
860
  val = simpledialog.askstring("Input", str(prompt))
1152
861
  root.destroy()
1153
862
  return val if val is not None else ""
1154
-
1155
863
  def visit_Confirm(self, node: Confirm):
1156
864
  prompt = self.visit(node.prompt)
1157
865
  root = tk.Tk()
@@ -1160,43 +868,24 @@ class Interpreter:
1160
868
  val = messagebox.askyesno("Confirm", str(prompt))
1161
869
  root.destroy()
1162
870
  return val
1163
-
1164
- # --- Async Features ---
1165
871
  def visit_Spawn(self, node: Spawn):
1166
- # Use a Future to hold result
1167
872
  executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
1168
-
1169
- # We need to capture the current env context if we want to share variables?
1170
- # But typical threading shares memory.
1171
- # self.visit uses self.current_env.
1172
- # Races are possible but expected in this simple model.
1173
-
1174
873
  future = executor.submit(self.visit, node.call)
1175
- # We don't shutdown executor immediately, let it hang or cache it?
1176
- # If we create new executor every time, it's inefficient but safe.
1177
874
  return future
1178
-
1179
875
  def visit_Await(self, node: Await):
1180
876
  task = self.visit(node.task)
1181
877
  if isinstance(task, concurrent.futures.Future):
1182
878
  return task.result()
1183
879
  raise TypeError(f"Cannot await non-task object: {type(task)}")
1184
-
1185
- # --- Regex ---
1186
880
  def visit_Regex(self, node: Regex):
1187
881
  return re.compile(node.pattern)
1188
-
1189
882
  def visit_FileWatcher(self, node: FileWatcher):
1190
- """Watch a file for changes"""
1191
883
  path = self.visit(node.path)
1192
884
  if not os.path.exists(path):
1193
- # Maybe wait for creation?
1194
- # For simplicity, error or wait.
1195
885
  print(f"Warning: Watching non-existent file {path}")
1196
886
  last_mtime = 0
1197
887
  else:
1198
888
  last_mtime = os.path.getmtime(path)
1199
-
1200
889
  try:
1201
890
  while True:
1202
891
  current_exists = os.path.exists(path)
@@ -1204,18 +893,14 @@ class Interpreter:
1204
893
  current_mtime = os.path.getmtime(path)
1205
894
  if current_mtime != last_mtime:
1206
895
  last_mtime = current_mtime
1207
- # Execute body
1208
896
  for stmt in node.body:
1209
897
  self.visit(stmt)
1210
-
1211
- time.sleep(1) # Poll every second
898
+ time.sleep(1)
1212
899
  except StopException:
1213
- pass # Allow stop to break watcher
900
+ pass
1214
901
  except ReturnException:
1215
902
  raise
1216
-
1217
903
  def _check_type(self, arg_name, val, type_hint):
1218
- """Runtime type checking"""
1219
904
  if type_hint == 'int' and not isinstance(val, int):
1220
905
  raise TypeError(f"Argument '{arg_name}' expects int, got {type(val).__name__}")
1221
906
  elif type_hint == 'str' and not isinstance(val, str):
@@ -1226,26 +911,17 @@ class Interpreter:
1226
911
  raise TypeError(f"Argument '{arg_name}' expects float, got {type(val).__name__}")
1227
912
  elif type_hint == 'list' and not isinstance(val, list):
1228
913
  raise TypeError(f"Argument '{arg_name}' expects list, got {type(val).__name__}")
1229
-
1230
-
1231
-
1232
914
  def visit_ConstAssign(self, node: ConstAssign):
1233
- """Assign a constant value that cannot be reassigned"""
1234
915
  value = self.visit(node.value)
1235
916
  self.current_env.set_const(node.name, value)
1236
917
  return value
1237
-
1238
918
  def visit_ForIn(self, node: ForIn):
1239
- """For-in loop: for x in iterable"""
1240
919
  iterable = self.visit(node.iterable)
1241
-
1242
920
  if not hasattr(iterable, '__iter__'):
1243
921
  raise TypeError(f"Cannot iterate over {type(iterable).__name__}")
1244
-
1245
922
  old_env = self.current_env
1246
923
  new_env = Environment(parent=self.current_env)
1247
924
  self.current_env = new_env
1248
-
1249
925
  try:
1250
926
  for item in iterable:
1251
927
  new_env.set(node.var_name, item)
@@ -1255,12 +931,9 @@ class Interpreter:
1255
931
  raise
1256
932
  finally:
1257
933
  self.current_env = old_env
1258
-
1259
934
  def visit_IndexAccess(self, node: IndexAccess):
1260
- """Array/dict index access: obj[index]"""
1261
935
  obj = self.visit(node.obj)
1262
936
  index = self.visit(node.index)
1263
-
1264
937
  if isinstance(obj, list):
1265
938
  if not isinstance(index, int):
1266
939
  raise TypeError(f"List indices must be integers, got {type(index).__name__}")
@@ -1273,24 +946,14 @@ class Interpreter:
1273
946
  return obj[index]
1274
947
  else:
1275
948
  raise TypeError(f"'{type(obj).__name__}' object is not subscriptable")
1276
-
1277
- # --- New English-like feature visitors ---
1278
-
1279
949
  def visit_Stop(self, node: Stop):
1280
- """Break out of a loop"""
1281
950
  raise StopException()
1282
-
1283
951
  def visit_Skip(self, node: Skip):
1284
- """Continue to next iteration"""
1285
952
  raise SkipException()
1286
-
1287
953
  def visit_Throw(self, node: Throw):
1288
- """Throw a custom error"""
1289
954
  message = self.visit(node.message)
1290
955
  raise ShellLiteError(str(message))
1291
-
1292
956
  def visit_Unless(self, node: Unless):
1293
- """Execute body if condition is FALSE"""
1294
957
  condition = self.visit(node.condition)
1295
958
  if not condition:
1296
959
  for stmt in node.body:
@@ -1298,9 +961,7 @@ class Interpreter:
1298
961
  elif node.else_body:
1299
962
  for stmt in node.else_body:
1300
963
  self.visit(stmt)
1301
-
1302
964
  def visit_Until(self, node: Until):
1303
- """Loop until condition becomes TRUE"""
1304
965
  while not self.visit(node.condition):
1305
966
  try:
1306
967
  for stmt in node.body:
@@ -1311,13 +972,10 @@ class Interpreter:
1311
972
  continue
1312
973
  except ReturnException:
1313
974
  raise
1314
-
1315
975
  def visit_Repeat(self, node: Repeat):
1316
- """Simple repeat N times loop"""
1317
976
  count = self.visit(node.count)
1318
977
  if not isinstance(count, int):
1319
978
  raise TypeError(f"repeat count must be an integer, got {type(count).__name__}")
1320
-
1321
979
  for _ in range(count):
1322
980
  try:
1323
981
  for stmt in node.body:
@@ -1328,71 +986,49 @@ class Interpreter:
1328
986
  continue
1329
987
  except ReturnException:
1330
988
  raise
1331
-
1332
989
  def visit_When(self, node: When):
1333
- """Pattern matching - when value is X => ... otherwise => ..."""
1334
990
  value = self.visit(node.value)
1335
-
1336
991
  for match_val, body in node.cases:
1337
992
  if self.visit(match_val) == value:
1338
993
  for stmt in body:
1339
994
  self.visit(stmt)
1340
995
  return
1341
-
1342
- # No match found, run otherwise
1343
996
  if node.otherwise:
1344
997
  for stmt in node.otherwise:
1345
998
  self.visit(stmt)
1346
-
1347
999
  def visit_Execute(self, node: Execute):
1348
- """Execute code from a string - execute 'say hello'"""
1349
1000
  code = self.visit(node.code)
1350
1001
  if not isinstance(code, str):
1351
1002
  raise TypeError(f"execute requires a string, got {type(code).__name__}")
1352
-
1353
1003
  from .lexer import Lexer
1354
1004
  from .parser import Parser
1355
-
1356
1005
  lexer = Lexer(code)
1357
1006
  tokens = lexer.tokenize()
1358
1007
  parser = Parser(tokens)
1359
1008
  statements = parser.parse()
1360
-
1361
1009
  result = None
1362
1010
  for stmt in statements:
1363
1011
  result = self.visit(stmt)
1364
1012
  return result
1365
-
1366
1013
  def visit_ImportAs(self, node: ImportAs):
1367
- """Import with alias: use 'math' as m"""
1368
1014
  if node.path in self.std_modules:
1369
1015
  self.current_env.set(node.alias, self.std_modules[node.path])
1370
1016
  return
1371
-
1372
- # Capture current functions to detect new ones
1373
1017
  old_funcs_keys = set(self.functions.keys())
1374
-
1375
- # Run module in isolated env
1376
1018
  module_env = Environment(parent=self.global_env)
1377
1019
  old_env = self.current_env
1378
1020
  self.current_env = module_env
1379
-
1380
- # Run module in isolated env
1381
1021
  module_env = Environment(parent=self.global_env)
1382
1022
  old_env = self.current_env
1383
1023
  self.current_env = module_env
1384
-
1385
- # 1. Check Local File
1386
1024
  if os.path.exists(node.path):
1387
1025
  target_path = node.path
1388
- # 2. Check Global Modules (~/.shell_lite/modules)
1389
1026
  else:
1390
1027
  home = os.path.expanduser("~")
1391
1028
  global_path = os.path.join(home, ".shell_lite", "modules", node.path)
1392
1029
  if os.path.exists(global_path):
1393
1030
  target_path = global_path
1394
1031
  else:
1395
- # Check if user omitted .shl extension for global module
1396
1032
  if not node.path.endswith('.shl'):
1397
1033
  global_path_ext = global_path + ".shl"
1398
1034
  if os.path.exists(global_path_ext):
@@ -1403,12 +1039,9 @@ class Interpreter:
1403
1039
  else:
1404
1040
  self.current_env = old_env
1405
1041
  raise FileNotFoundError(f"Could not find imported file: {node.path} (searched local and global modules)")
1406
-
1407
- # Folder Support: If it's a directory, assume main.shl
1408
1042
  if os.path.isdir(target_path):
1409
1043
  main_shl = os.path.join(target_path, "main.shl")
1410
1044
  pkg_shl = os.path.join(target_path, f"{os.path.basename(target_path)}.shl")
1411
-
1412
1045
  if os.path.exists(main_shl):
1413
1046
  target_path = main_shl
1414
1047
  elif os.path.exists(pkg_shl):
@@ -1416,47 +1049,31 @@ class Interpreter:
1416
1049
  else:
1417
1050
  self.current_env = old_env
1418
1051
  raise FileNotFoundError(f"Package '{node.path}' is a folder but has no 'main.shl' or '{os.path.basename(target_path)}.shl'.")
1419
-
1420
1052
  try:
1421
1053
  with open(target_path, 'r', encoding='utf-8') as f:
1422
1054
  code = f.read()
1423
-
1424
1055
  from .lexer import Lexer
1425
1056
  from .parser import Parser
1426
-
1427
1057
  lexer = Lexer(code)
1428
1058
  tokens = lexer.tokenize()
1429
1059
  parser = Parser(tokens)
1430
1060
  statements = parser.parse()
1431
-
1432
1061
  for stmt in statements:
1433
1062
  self.visit(stmt)
1434
-
1435
- # Create module object (dict)
1436
1063
  module_exports = {}
1437
- # 1. Variables
1438
1064
  module_exports.update(module_env.variables)
1439
-
1440
- # 2. Functions (Global pollution management)
1441
1065
  current_funcs_keys = set(self.functions.keys())
1442
1066
  new_funcs = current_funcs_keys - old_funcs_keys
1443
-
1444
1067
  for fname in new_funcs:
1445
1068
  func_node = self.functions[fname]
1446
1069
  module_exports[fname] = func_node
1447
- # Remove from global to avoid pollution
1448
1070
  del self.functions[fname]
1449
-
1450
- # Restore Env
1451
1071
  self.current_env = old_env
1452
1072
  self.current_env.set(node.alias, module_exports)
1453
-
1454
1073
  except Exception as e:
1455
1074
  self.current_env = old_env
1456
1075
  raise RuntimeError(f"Failed to import '{node.path}': {e}")
1457
-
1458
1076
  def visit_Forever(self, node: Forever):
1459
- """Infinite loop - forever"""
1460
1077
  while True:
1461
1078
  try:
1462
1079
  for stmt in node.body:
@@ -1467,176 +1084,118 @@ class Interpreter:
1467
1084
  continue
1468
1085
  except ReturnException:
1469
1086
  raise
1470
-
1471
1087
  def visit_Exit(self, node: Exit):
1472
- """Exit the program - exit or exit 1"""
1473
1088
  code = 0
1474
1089
  if node.code:
1475
1090
  code = self.visit(node.code)
1476
1091
  import sys
1477
1092
  sys.exit(code)
1478
-
1479
1093
  def visit_Make(self, node: Make):
1480
- """Create object - make Robot 'name' 100"""
1481
1094
  if node.class_name not in self.classes:
1482
1095
  raise NameError(f"Thing '{node.class_name}' not defined.")
1483
-
1484
1096
  class_def = self.classes[node.class_name]
1485
1097
  props = self._get_class_properties(class_def)
1486
-
1487
1098
  if len(node.args) != len(props):
1488
1099
  raise TypeError(f"Thing '{node.class_name}' expects {len(props)} values, got {len(node.args)}")
1489
-
1490
1100
  instance = Instance(class_def)
1491
1101
  for prop, arg in zip(props, node.args):
1492
1102
  instance.data[prop] = self.visit(arg)
1493
-
1494
1103
  return instance
1495
-
1496
1104
  def visit_Convert(self, node: Convert):
1497
1105
  val = self.visit(node.expression)
1498
-
1499
1106
  if node.target_format.lower() == 'json':
1500
1107
  if isinstance(val, str):
1501
- # Convert string TO json object (parse)
1502
1108
  try:
1503
1109
  return json.loads(val)
1504
- except: # If it fails, maybe they meant to stringify? No, "convert X to json" usually means make it json string?
1505
- # Ambiguity: "convert dict to json" -> stringify
1506
- # "convert string to json" -> parse?
1507
- # Prompt said: json_string = convert my_data to json
1508
- # So it means SERIALIZE (stringify)
1110
+ except:
1509
1111
  return json.dumps(val)
1510
1112
  else:
1511
- # Serialize
1512
1113
  if isinstance(val, Instance):
1513
1114
  return json.dumps(val.data)
1514
- return json.dumps(val) # default
1515
-
1115
+ return json.dumps(val)
1516
1116
  raise ValueError(f"Unknown conversion format: {node.target_format}")
1517
-
1518
1117
  def visit_ProgressLoop(self, node: ProgressLoop):
1519
- # We need to wrap the loop execution with progress bar
1520
- # How to hook into iteration?
1521
- # Only support For, ForIn, Repeat
1522
-
1523
1118
  loop = node.loop_node
1524
-
1525
1119
  if isinstance(loop, Repeat):
1526
1120
  count = self.visit(loop.count)
1527
1121
  if not isinstance(count, int): count = 0
1528
-
1529
1122
  print(f"Progress: [ ] 0%", end='\r')
1530
1123
  for i in range(count):
1531
- # update bar
1532
1124
  percent = int((i / count) * 100)
1533
1125
  bar = '=' * int(percent / 5)
1534
1126
  print(f"Progress: [{bar:<20}] {percent}%", end='\r')
1535
-
1536
- # Exec body
1537
1127
  try:
1538
1128
  for stmt in loop.body:
1539
1129
  self.visit(stmt)
1540
- except: pass # Allow clean exit
1541
-
1130
+ except: pass
1542
1131
  print(f"Progress: [{'='*20}] 100% ")
1543
-
1544
1132
  elif isinstance(loop, For):
1545
- # range based
1546
1133
  count = self.visit(loop.count)
1547
1134
  for i in range(count):
1548
1135
  percent = int((i / count) * 100)
1549
1136
  bar = '=' * int(percent / 5)
1550
1137
  print(f"Progress: [{bar:<20}] {percent}%", end='\r')
1551
-
1552
1138
  try:
1553
1139
  for stmt in loop.body:
1554
1140
  self.visit(stmt)
1555
1141
  except: pass
1556
1142
  print(f"Progress: [{'='*20}] 100% ")
1557
-
1558
1143
  elif isinstance(loop, ForIn):
1559
- # We need length of iterable
1560
1144
  iterable = self.visit(loop.iterable)
1561
1145
  total = len(iterable) if hasattr(iterable, '__len__') else 0
1562
-
1563
- # Can't use generator directly if we want progress, convert to list if needed?
1564
- # Or just iterate
1565
-
1566
1146
  i = 0
1567
1147
  for item in iterable:
1568
1148
  if total > 0:
1569
1149
  percent = int((i / total) * 100)
1570
1150
  bar = '=' * int(percent / 5)
1571
1151
  print(f"Progress: [{bar:<20}] {percent}%", end='\r')
1572
-
1573
- # Bind var
1574
1152
  self.current_env.set(loop.var_name, item)
1575
-
1576
1153
  try:
1577
1154
  for stmt in loop.body:
1578
1155
  self.visit(stmt)
1579
1156
  except: pass
1580
-
1581
1157
  i += 1
1582
1158
  if total > 0:
1583
1159
  print(f"Progress: [{'='*20}] 100% ")
1584
-
1585
-
1586
1160
  def visit_DatabaseOp(self, node: DatabaseOp):
1587
1161
  if node.op == 'open':
1588
1162
  path = self.visit(node.args[0])
1589
1163
  self.db_conn = sqlite3.connect(path, check_same_thread=False)
1590
1164
  return self.db_conn
1591
-
1592
1165
  elif node.op == 'close':
1593
1166
  if self.db_conn:
1594
1167
  self.db_conn.close()
1595
1168
  self.db_conn = None
1596
-
1597
1169
  elif node.op == 'exec':
1598
1170
  if not self.db_conn:
1599
1171
  raise RuntimeError("Database not open. Use 'db open \"path\"' first.")
1600
-
1601
1172
  sql = self.visit(node.args[0])
1602
1173
  params = [self.visit(arg) for arg in node.args[1:]]
1603
-
1604
1174
  cursor = self.db_conn.cursor()
1605
1175
  cursor.execute(sql, params)
1606
1176
  self.db_conn.commit()
1607
1177
  return cursor.lastrowid
1608
-
1609
1178
  elif node.op == 'query':
1610
1179
  if not self.db_conn:
1611
1180
  raise RuntimeError("Database not open. Use 'db open \"path\"' first.")
1612
-
1613
1181
  sql = self.visit(node.args[0])
1614
1182
  params = [self.visit(arg) for arg in node.args[1:]]
1615
-
1616
1183
  cursor = self.db_conn.cursor()
1617
1184
  cursor.execute(sql, params)
1618
1185
  columns = [description[0] for description in cursor.description] if cursor.description else []
1619
1186
  rows = cursor.fetchall()
1620
-
1621
- # Return list of dictionaries
1622
1187
  result = []
1623
1188
  for row in rows:
1624
1189
  result.append(dict(zip(columns, row)))
1625
1190
  return result
1626
-
1627
1191
  def visit_ServeStatic(self, node: ServeStatic):
1628
1192
  folder = str(self.visit(node.folder))
1629
1193
  url_prefix = str(self.visit(node.url))
1630
-
1631
- # Ensure url prefix starts with /
1632
1194
  if not url_prefix.startswith('/'): url_prefix = '/' + url_prefix
1633
-
1634
1195
  if not os.path.isdir(folder):
1635
1196
  print(f"Warning: Static folder '{folder}' does not exist.")
1636
-
1637
1197
  self.static_routes[url_prefix] = folder
1638
1198
  print(f"Serving static files from '{folder}' at '{url_prefix}'")
1639
-
1640
1199
  def visit_Every(self, node: Every):
1641
1200
  interval = self.visit(node.interval)
1642
1201
  if node.unit == 'minutes': interval *= 60
@@ -1645,87 +1204,66 @@ class Interpreter:
1645
1204
  for stmt in node.body: self.visit(stmt)
1646
1205
  time.sleep(interval)
1647
1206
  except KeyboardInterrupt: pass
1648
-
1649
1207
  def visit_After(self, node: After):
1650
1208
  delay = self.visit(node.delay)
1651
1209
  if node.unit == 'minutes': delay *= 60
1652
1210
  time.sleep(delay)
1653
1211
  for stmt in node.body: self.visit(stmt)
1654
-
1655
1212
  def visit_OnRequest(self, node: OnRequest):
1656
- # path can be string pattern: "/users/:id"
1657
1213
  path_str = self.visit(node.path)
1658
-
1659
1214
  if path_str == '__middleware__':
1660
1215
  self.middleware_routes.append(node.body)
1661
1216
  return
1662
-
1663
1217
  regex_pattern = "^" + path_str + "$"
1664
1218
  if ':' in path_str:
1665
1219
  regex_pattern = "^" + re.sub(r':(\w+)', r'(?P<\1>[^/]+)', path_str) + "$"
1666
-
1667
1220
  compiled = re.compile(regex_pattern)
1668
1221
  self.http_routes.append((path_str, compiled, node.body))
1669
-
1670
1222
  def visit_Listen(self, node: Listen):
1671
1223
  port_val = self.visit(node.port)
1672
1224
  interpreter_ref = self
1673
-
1674
1225
  class ShellLiteHandler(BaseHTTPRequestHandler):
1675
1226
  def log_message(self, format, *args): pass
1676
-
1677
1227
  def do_GET(self):
1678
1228
  self.handle_req()
1679
-
1680
1229
  def do_POST(self):
1681
- # Parse POST data
1682
1230
  content_length = int(self.headers.get('Content-Length', 0))
1683
1231
  content_type = self.headers.get('Content-Type', '')
1684
1232
  post_data = self.rfile.read(content_length).decode('utf-8')
1685
-
1686
1233
  params = {}
1687
1234
  json_data = None
1688
-
1689
1235
  if 'application/json' in content_type:
1690
1236
  try:
1691
1237
  json_data = json.loads(post_data)
1692
1238
  except:
1693
1239
  pass
1694
1240
  else:
1695
- # Parse params (x-www-form-urlencoded)
1696
1241
  if post_data:
1697
1242
  parsed = urllib.parse.parse_qs(post_data)
1698
1243
  params = {k: v[0] for k, v in parsed.items()}
1699
-
1700
1244
  self.handle_req(params, json_data)
1701
-
1245
+ def do_HEAD(self):
1246
+ self.handle_req()
1702
1247
  def handle_req(self, post_params=None, json_data=None):
1703
1248
  try:
1704
1249
  if post_params is None: post_params = {}
1705
1250
  path = self.path
1706
1251
  if '?' in path: path = path.split('?')[0]
1707
-
1708
- # Create request object
1709
1252
  req_obj = {
1710
- "method": self.command,
1253
+ "method": self.command,
1711
1254
  "path": path,
1712
1255
  "params": post_params,
1713
- "form": post_params, # Alias for form data
1256
+ "form": post_params,
1714
1257
  "json": json_data
1715
1258
  }
1716
1259
  interpreter_ref.global_env.set("request", req_obj)
1717
-
1718
- # Also keep legacy separate vars for compatibility if needed
1719
- interpreter_ref.global_env.set("REQUEST_METHOD", self.command) # GET or POST
1720
-
1721
- # 1. Static Routes
1260
+ interpreter_ref.global_env.set("REQUEST_METHOD", self.command)
1722
1261
  for prefix, folder in interpreter_ref.static_routes.items():
1723
1262
  if path.startswith(prefix):
1724
1263
  clean_path = path[len(prefix):]
1725
1264
  if clean_path.startswith('/'): clean_path = clean_path[1:]
1726
1265
  if clean_path == '': clean_path = 'index.html'
1727
1266
  file_path = os.path.join(folder, clean_path)
1728
-
1729
1267
  if os.path.exists(file_path) and os.path.isfile(file_path):
1730
1268
  self.send_response(200)
1731
1269
  ct = 'application/octet-stream'
@@ -1734,10 +1272,9 @@ class Interpreter:
1734
1272
  elif file_path.endswith('.js'): ct = 'application/javascript'
1735
1273
  self.send_header('Content-Type', ct)
1736
1274
  self.end_headers()
1737
- with open(file_path, 'rb') as f: self.wfile.write(f.read())
1275
+ if self.command != 'HEAD':
1276
+ with open(file_path, 'rb') as f: self.wfile.write(f.read())
1738
1277
  return
1739
-
1740
- # 2. Dynamic Routing
1741
1278
  matched_body = None
1742
1279
  path_params = {}
1743
1280
  for pattern, regex, body in interpreter_ref.http_routes:
@@ -1746,45 +1283,36 @@ class Interpreter:
1746
1283
  matched_body = body
1747
1284
  path_params = match.groupdict()
1748
1285
  break
1749
-
1750
1286
  if matched_body:
1751
- # Middleware
1752
1287
  for mw in interpreter_ref.middleware_routes:
1753
1288
  for stmt in mw: interpreter_ref.visit(stmt)
1754
-
1755
- # Inject Params (Path and POST)
1756
1289
  for k, v in path_params.items():
1757
1290
  interpreter_ref.global_env.set(k, v)
1758
1291
  for k, v in post_params.items():
1759
1292
  interpreter_ref.global_env.set(k, v)
1760
-
1761
1293
  interpreter_ref.web.stack = []
1762
1294
  response_body = ""
1763
-
1764
1295
  result = None
1765
1296
  try:
1766
1297
  for stmt in matched_body:
1767
1298
  result = interpreter_ref.visit(stmt)
1768
1299
  except ReturnException as re:
1769
1300
  result = re.value
1770
-
1771
1301
  if interpreter_ref.web.stack:
1772
- # If stuff left in stack (unlikely if popped correctly), usually we explicitly return Tags
1773
1302
  pass
1774
-
1775
1303
  if isinstance(result, Tag): response_body = str(result)
1776
1304
  elif result: response_body = str(result)
1777
1305
  else: response_body = "OK"
1778
-
1779
1306
  self.send_response(200)
1780
1307
  self.send_header('Content-Type', 'text/html')
1781
1308
  self.end_headers()
1782
- self.wfile.write(response_body.encode())
1309
+ if self.command != 'HEAD':
1310
+ self.wfile.write(response_body.encode())
1783
1311
  else:
1784
1312
  self.send_response(404)
1785
1313
  self.end_headers()
1786
- self.wfile.write(b'Not Found')
1787
-
1314
+ if self.command != 'HEAD':
1315
+ self.wfile.write(b'Not Found')
1788
1316
  except Exception as e:
1789
1317
  print(f"DEBUG: Server Exception: {e}")
1790
1318
  import traceback
@@ -1792,14 +1320,16 @@ class Interpreter:
1792
1320
  try:
1793
1321
  self.send_response(500)
1794
1322
  self.end_headers()
1795
- self.wfile.write(str(e).encode())
1323
+ if self.command != 'HEAD':
1324
+ self.wfile.write(str(e).encode())
1796
1325
  except: pass
1797
-
1798
1326
  server = HTTPServer(('0.0.0.0', port_val), ShellLiteHandler)
1799
- print(f"ShellLite Server running on port {port_val}...")
1327
+ print(f"\n ShellLite Server v0.03.4 is running!")
1328
+ print(f" \u001b[1;36m➜\u001b[0m Local: \u001b[1;4;36mhttp://localhost:{port_val}/\u001b[0m\n")
1800
1329
  try: server.serve_forever()
1801
- except KeyboardInterrupt: pass
1802
-
1330
+ except KeyboardInterrupt:
1331
+ print("\n Server stopped.")
1332
+ pass
1803
1333
  def visit_DatabaseOp(self, node: DatabaseOp):
1804
1334
  if node.op == 'open':
1805
1335
  path = self.visit(node.args[0])
@@ -1827,7 +1357,6 @@ class Interpreter:
1827
1357
  params = val if isinstance(val, list) else [val]
1828
1358
  c = self.db_conn.cursor(); c.execute(sql, params)
1829
1359
  return c.fetchall()
1830
-
1831
1360
  def visit_Download(self, node: Download):
1832
1361
  url = self.visit(node.url)
1833
1362
  filename = url.split('/')[-1] or "downloaded_file"
@@ -1843,11 +1372,9 @@ class Interpreter:
1843
1372
  print(f"Error: Permission denied writing to {filename}.")
1844
1373
  except Exception as e:
1845
1374
  print(f"Error: Download failed: {e}")
1846
-
1847
1375
  def visit_ArchiveOp(self, node: ArchiveOp):
1848
1376
  source = str(self.visit(node.source))
1849
1377
  target = str(self.visit(node.target))
1850
-
1851
1378
  try:
1852
1379
  if node.op == 'compress':
1853
1380
  print(f"Compressing '{source}' to '{target}'...")
@@ -1860,12 +1387,11 @@ class Interpreter:
1860
1387
  print(f"Error: Source '{source}' does not exist.")
1861
1388
  return
1862
1389
  print("Compression complete.")
1863
- else: # extract
1390
+ else:
1864
1391
  print(f"Extracting '{source}' to '{target}'...")
1865
1392
  if not os.path.exists(source):
1866
1393
  print(f"Error: Archive '{source}' does not exist.")
1867
1394
  return
1868
-
1869
1395
  with zipfile.ZipFile(source, 'r') as zipf:
1870
1396
  zipf.extractall(target)
1871
1397
  print("Extraction complete.")
@@ -1873,22 +1399,17 @@ class Interpreter:
1873
1399
  print(f"Error: '{source}' is not a valid zip file.")
1874
1400
  except Exception as e:
1875
1401
  print(f"Error: Archive operation failed: {e}")
1876
-
1877
1402
  def visit_CsvOp(self, node: CsvOp):
1878
1403
  path = self.visit(node.path)
1879
-
1880
1404
  if node.op == 'load':
1881
1405
  with open(path, 'r', newline='') as f:
1882
1406
  reader = csv.DictReader(f)
1883
1407
  return [row for row in reader]
1884
- else: # save
1408
+ else:
1885
1409
  data = self.visit(node.data)
1886
1410
  if not isinstance(data, list):
1887
- data = [data] # wrap single object
1888
-
1889
- if not data: return # Nothing to write
1890
-
1891
- # Normalize data: if it's Instance objects, conv to dict
1411
+ data = [data]
1412
+ if not data: return
1892
1413
  rows = []
1893
1414
  for item in data:
1894
1415
  if isinstance(item, Instance):
@@ -1900,7 +1421,6 @@ class Interpreter:
1900
1421
  else:
1901
1422
  print("Error: Only lists of objects/dictionaries can be saved to CSV.")
1902
1423
  return
1903
-
1904
1424
  if rows:
1905
1425
  try:
1906
1426
  keys = rows[0].keys()
@@ -1911,53 +1431,40 @@ class Interpreter:
1911
1431
  print(f"Saved {len(rows)} rows to '{path}'.")
1912
1432
  except Exception as e:
1913
1433
  print(f"Error saving CSV: {e}")
1914
-
1915
1434
  def visit_ClipboardOp(self, node: ClipboardOp):
1916
1435
  if 'pyperclip' not in sys.modules:
1917
1436
  raise RuntimeError("Install 'pyperclip' for clipboard support.")
1918
-
1919
1437
  if node.op == 'copy':
1920
1438
  content = str(self.visit(node.content))
1921
1439
  pyperclip.copy(content)
1922
1440
  else:
1923
1441
  return pyperclip.paste()
1924
-
1925
1442
  def visit_AutomationOp(self, node: AutomationOp):
1926
1443
  args = [self.visit(a) for a in node.args]
1927
-
1928
1444
  if node.action == 'press':
1929
1445
  if 'keyboard' not in sys.modules: raise RuntimeError("Install 'keyboard'")
1930
1446
  keyboard.press_and_release(args[0])
1931
-
1932
1447
  elif node.action == 'type':
1933
1448
  if 'keyboard' not in sys.modules: raise RuntimeError("Install 'keyboard'")
1934
1449
  keyboard.write(str(args[0]))
1935
-
1936
1450
  elif node.action == 'click':
1937
1451
  if 'mouse' not in sys.modules: raise RuntimeError("Install 'mouse'")
1938
1452
  mouse.move(args[0], args[1], absolute=True, duration=0.2)
1939
1453
  mouse.click('left')
1940
-
1941
1454
  elif node.action == 'notify':
1942
1455
  if 'plyer' not in sys.modules: raise RuntimeError("Install 'plyer'")
1943
1456
  notification.notify(title=str(args[0]), message=str(args[1]))
1944
-
1945
1457
  def visit_DateOp(self, node: DateOp):
1946
1458
  if node.expr == 'today':
1947
1459
  return datetime.now().strftime("%Y-%m-%d")
1948
-
1949
- # Simple parser for "next friday", "tomorrow"
1950
- # This is basic logic; proper NLP date parsing (like dateparser lib) is better but heavy.
1951
1460
  today = datetime.now()
1952
1461
  s = node.expr.lower().strip()
1953
-
1954
1462
  if s == 'tomorrow':
1955
1463
  d = today + timedelta(days=1)
1956
1464
  return d.strftime("%Y-%m-%d")
1957
1465
  elif s == 'yesterday':
1958
1466
  d = today - timedelta(days=1)
1959
1467
  return d.strftime("%Y-%m-%d")
1960
-
1961
1468
  days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
1962
1469
  if s.startswith('next '):
1963
1470
  day_str = s.replace('next ', '').strip()
@@ -1968,23 +1475,17 @@ class Interpreter:
1968
1475
  if days_ahead <= 0: days_ahead += 7
1969
1476
  d = today + timedelta(days=days_ahead)
1970
1477
  return d.strftime("%Y-%m-%d")
1971
-
1972
- return s # Fallback checks?
1973
-
1478
+ return s
1974
1479
  def visit_FileWrite(self, node: FileWrite):
1975
- """Write or Append to file"""
1976
1480
  path = str(self.visit(node.path))
1977
1481
  content = str(self.visit(node.content))
1978
-
1979
1482
  try:
1980
1483
  with open(path, node.mode, encoding='utf-8') as f:
1981
1484
  f.write(content)
1982
1485
  print(f"{'Appended to' if node.mode == 'a' else 'Written to'} file '{path}'")
1983
1486
  except Exception as e:
1984
1487
  raise RuntimeError(f"File operation failed: {e}")
1985
-
1986
1488
  def visit_FileRead(self, node: FileRead):
1987
- """Read file content"""
1988
1489
  path = str(self.visit(node.path))
1989
1490
  try:
1990
1491
  with open(path, 'r', encoding='utf-8') as f:
@@ -1992,23 +1493,19 @@ class Interpreter:
1992
1493
  except FileNotFoundError:
1993
1494
  raise FileNotFoundError(f"File '{path}' not found.")
1994
1495
  raise RuntimeError(f"Read failed: {e}")
1995
-
1996
1496
  if __name__ == '__main__':
1997
1497
  import sys
1998
1498
  if len(sys.argv) < 2:
1999
1499
  print("Usage: python -m src.interpreter <file.shl>")
2000
1500
  sys.exit(1)
2001
-
2002
1501
  filename = sys.argv[1]
2003
1502
  try:
2004
1503
  with open(filename, 'r', encoding='utf-8') as f:
2005
1504
  code = f.read()
2006
-
2007
1505
  lexer = Lexer(code)
2008
1506
  tokens = lexer.tokenize()
2009
1507
  parser = Parser(tokens)
2010
1508
  ast = parser.parse()
2011
-
2012
1509
  interpreter = Interpreter()
2013
1510
  for stmt in ast:
2014
1511
  interpreter.visit(stmt)