shell-lite 0.4.1__py3-none-any.whl → 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1773 @@
1
+ from typing import Any, Dict, List, Callable
2
+ from .ast_nodes import *
3
+ from .lexer_new import Token, Lexer
4
+ from .parser_new import Parser
5
+ import importlib
6
+ import operator
7
+ import re
8
+ import os
9
+ import sys
10
+ import subprocess
11
+ import json
12
+ import math
13
+ import time
14
+ import random
15
+ import urllib.request
16
+ import urllib.parse
17
+ import shutil
18
+ import functools
19
+ from datetime import datetime
20
+ import threading
21
+ import concurrent.futures
22
+ import tkinter as tk
23
+ from tkinter import messagebox, simpledialog
24
+ from http.server import HTTPServer, BaseHTTPRequestHandler
25
+ import csv
26
+ import zipfile
27
+ from datetime import timedelta
28
+ import calendar
29
+ import sqlite3
30
+ try:
31
+ import keyboard
32
+ import mouse
33
+ import pyperclip
34
+ from plyer import notification
35
+ except ImportError:
36
+ pass
37
+ class Environment:
38
+ def __init__(self, parent=None):
39
+ self.variables: Dict[str, Any] = {}
40
+ self.constants: set = set()
41
+ self.parent = parent
42
+ def get(self, name: str) -> Any:
43
+ if name in self.variables:
44
+ return self.variables[name]
45
+ if self.parent:
46
+ return self.parent.get(name)
47
+ raise NameError(f"Variable '{name}' is not defined.")
48
+ def set(self, name: str, value: Any):
49
+ if name in self.constants:
50
+ raise RuntimeError(f"Cannot reassign constant '{name}'")
51
+ if self.parent and name in self.parent.constants:
52
+ raise RuntimeError(f"Cannot reassign constant '{name}'")
53
+ self.variables[name] = value
54
+ def set_const(self, name: str, value: Any):
55
+ if name in self.variables:
56
+ raise RuntimeError(f"Constant '{name}' already declared")
57
+ self.variables[name] = value
58
+ self.constants.add(name)
59
+ class ReturnException(Exception):
60
+ def __init__(self, value):
61
+ self.value = value
62
+ class StopException(Exception):
63
+ pass
64
+ class SkipException(Exception):
65
+ pass
66
+ class ShellLiteError(Exception):
67
+ def __init__(self, message):
68
+ self.message = message
69
+ super().__init__(message)
70
+ class LambdaFunction:
71
+ def __init__(self, params: List[str], body, interpreter):
72
+ self.params = params
73
+ self.body = body
74
+ self.interpreter = interpreter
75
+ self.closure_env = interpreter.current_env
76
+ def __call__(self, *args):
77
+ if len(args) != len(self.params):
78
+ raise TypeError(f"Lambda expects {len(self.params)} args, got {len(args)}")
79
+ old_env = self.interpreter.current_env
80
+ new_env = Environment(parent=self.closure_env)
81
+ for param, arg in zip(self.params, args):
82
+ new_env.set(param, arg)
83
+ self.interpreter.current_env = new_env
84
+ try:
85
+ result = self.interpreter.visit(self.body)
86
+ finally:
87
+ self.interpreter.current_env = old_env
88
+ return result
89
+ class Instance:
90
+ def __init__(self, class_def: ClassDef):
91
+ self.class_def = class_def
92
+ self.data: Dict[str, Any] = {}
93
+ class Tag:
94
+ def __init__(self, name: str, attrs: Dict[str, Any] = None):
95
+ self.name = name
96
+ self.attrs = attrs or {}
97
+ self.children: List[Any] = []
98
+ def add(self, child):
99
+ if isinstance(child, Tag):
100
+ if any(c is child for c in self.children):
101
+ return
102
+ self.children.append(child)
103
+ def __str__(self):
104
+ attr_str = ""
105
+ for k, v in self.attrs.items():
106
+ attr_str += f' {k}="{v}"'
107
+ inner = ""
108
+ for child in self.children:
109
+ inner += str(child)
110
+ if self.name in ('img', 'br', 'hr', 'input', 'meta', 'link'):
111
+ return f"<{self.name}{attr_str} />"
112
+ return f"<{self.name}{attr_str}>{inner}</{self.name}>"
113
+ class WebBuilder:
114
+ def __init__(self, interpreter):
115
+ self.stack: List[Tag] = []
116
+ self.interpreter = interpreter
117
+ def push(self, tag: Tag):
118
+ if self.stack:
119
+ self.stack[-1].add(tag)
120
+ self.stack.append(tag)
121
+ def pop(self):
122
+ if not self.stack: return None
123
+ return self.stack.pop()
124
+ def add_text(self, text: str):
125
+ if self.stack:
126
+ self.stack[-1].add(text)
127
+ else:
128
+ pass
129
+ class Interpreter:
130
+ def __init__(self):
131
+ print('DEBUG: ShellLite v0.04.3')
132
+ self.global_env = Environment()
133
+ self.global_env.set('str', str)
134
+ self.global_env.set('int', int)
135
+ self.global_env.set('float', float)
136
+ self.global_env.set('list', list)
137
+ self.global_env.set('len', len)
138
+ self.global_env.set('input', input)
139
+ self.global_env.set('range', range)
140
+
141
+ # English-like helpers
142
+ self.global_env.set('wait', time.sleep)
143
+ self.global_env.set('append', lambda l, x: l.append(x))
144
+ self.global_env.set('remove', lambda l, x: l.remove(x))
145
+ self.global_env.set('empty', lambda l: len(l) == 0)
146
+ self.global_env.set('contains', lambda l, x: x in l)
147
+
148
+ self.current_env = self.global_env
149
+ self.functions: Dict[str, FunctionDef] = {}
150
+ self.classes: Dict[str, ClassDef] = {}
151
+ self.http_routes = []
152
+ self.middleware_routes = []
153
+ self.static_routes = {}
154
+ self.web = WebBuilder(self)
155
+ self.db_conn = None
156
+ self.builtins = {
157
+ 'str': str, 'int': int, 'float': float, 'bool': bool,
158
+ 'list': list, 'len': len,
159
+ 'range': lambda *args: list(range(*args)),
160
+ 'typeof': lambda x: type(x).__name__,
161
+ 'run': self.builtin_run,
162
+ 'read': self.builtin_read,
163
+ 'write': self.builtin_write,
164
+ 'json_parse': self.builtin_json_parse,
165
+ 'json_stringify': self.builtin_json_stringify,
166
+ 'print': print,
167
+ 'abs': abs, 'min': min, 'max': max,
168
+ 'round': round, 'pow': pow, 'sum': sum,
169
+ 'split': lambda s, d=" ": s.split(d),
170
+ 'join': lambda lst, d="": d.join(str(x) for x in lst),
171
+ 'replace': lambda s, old, new: s.replace(old, new),
172
+ 'upper': self._builtin_upper,
173
+ 'lower': lambda s: s.lower(),
174
+ 'trim': lambda s: s.strip(),
175
+ 'startswith': lambda s, p: s.startswith(p),
176
+ 'endswith': lambda s, p: s.endswith(p),
177
+ 'sum_range': self._builtin_sum_range,
178
+ 'range_list': self._builtin_range_list,
179
+ 'find': lambda s, sub: s.find(sub),
180
+ 'char': chr, 'ord': ord,
181
+ 'append': lambda l, x: (l.append(x), l)[1],
182
+ 'push': self._builtin_push,
183
+ 'count': len,
184
+ 'remove': lambda l, x: l.remove(x),
185
+ 'pop': lambda l, idx=-1: l.pop(idx),
186
+ 'get': lambda l, idx: l[idx],
187
+ 'set': lambda l, idx, val: l.__setitem__(idx, val) or l,
188
+ 'sort': lambda l: sorted(l),
189
+ 'reverse': lambda l: list(reversed(l)),
190
+ 'slice': lambda l, start, end=None: l[start:end],
191
+ 'contains': lambda l, x: x in l,
192
+ 'index': lambda l, x: l.index(x) if x in l else -1,
193
+ 'exists': os.path.exists,
194
+ 'delete': os.remove,
195
+ 'copy': shutil.copy,
196
+ 'rename': os.rename,
197
+ 'mkdir': lambda p: os.makedirs(p, exist_ok=True),
198
+ 'listdir': os.listdir,
199
+ 'http_get': self.builtin_http_get,
200
+ 'http_post': self.builtin_http_post,
201
+ 'random': random.random,
202
+ 'randint': random.randint,
203
+ 'sleep': time.sleep,
204
+ 'now': lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
205
+ 'timestamp': time.time,
206
+ 'unique': lambda l: list(dict.fromkeys(l)),
207
+ 'first': lambda l: l[0] if l else None,
208
+ 'last': lambda l: l[-1] if l else None,
209
+ 'empty': lambda x: len(x) == 0 if hasattr(x, '__len__') else x is None,
210
+ 'keys': lambda d: list(d.keys()),
211
+ 'values': lambda d: list(d.values()),
212
+ 'items': lambda d: list(d.items()),
213
+ 'wait': time.sleep,
214
+ 'wait': time.sleep,
215
+ 'push': self._builtin_push,
216
+ 'remove': lambda lst, item: lst.remove(item),
217
+ 'Set': set,
218
+ 'show': print,
219
+ 'say': print,
220
+ 'today': lambda: datetime.now().strftime("%Y-%m-%d"),
221
+ }
222
+ tags = [
223
+ 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
224
+ 'span', 'a', 'img', 'button', 'input', 'form',
225
+ 'ul', 'li', 'ol', 'table', 'tr', 'td', 'th',
226
+ 'html', 'head', 'body', 'title', 'meta', 'link',
227
+ 'script', 'style', 'br', 'hr',
228
+ 'header', 'footer', 'section', 'article', 'nav', 'aside', 'main',
229
+ 'strong', 'em', 'code', 'pre', 'blockquote', 'iframe', 'canvas', 'svg',
230
+ 'css', 'textarea', 'label'
231
+ ]
232
+ for t in tags:
233
+ self.builtins[t] = self._make_tag_fn(t)
234
+ self.builtins['env'] = lambda name: os.environ.get(str(name), None)
235
+ self.builtins['int'] = lambda x: int(float(x)) if x else 0
236
+ self.builtins['str'] = lambda x: str(x)
237
+ class TimeWrapper:
238
+ def now(self):
239
+ return str(int(time.time()))
240
+ self.builtins['time'] = TimeWrapper()
241
+ self._init_std_modules()
242
+ for k, v in self.builtins.items():
243
+ self.global_env.set(k, v)
244
+ def _make_tag_fn(self, tag_name):
245
+ def tag_fn(*args):
246
+ attrs = {}
247
+ content = []
248
+ for arg in args:
249
+ if isinstance(arg, dict):
250
+ attrs.update(arg)
251
+ elif isinstance(arg, str):
252
+ if '=' in arg and not ' ' in arg and arg.split('=')[0].isalnum():
253
+ k, v = arg.split('=', 1)
254
+ attrs[k] = v
255
+ else:
256
+ content.append(arg)
257
+ else:
258
+ content.append(str(arg))
259
+ t = Tag(tag_name, attrs)
260
+ for c in content:
261
+ t.add(c)
262
+ return t
263
+ return tag_fn
264
+ def _builtin_map(self, lst, func):
265
+ if callable(func):
266
+ return [func(x) for x in lst]
267
+ raise TypeError("map requires a callable")
268
+ def _builtin_filter(self, lst, func):
269
+ if callable(func):
270
+ return [x for x in lst if func(x)]
271
+ raise TypeError("filter requires a callable")
272
+ def _builtin_reduce(self, lst, func, initial=None):
273
+ if callable(func):
274
+ if initial is not None:
275
+ return functools.reduce(func, lst, initial)
276
+ return functools.reduce(func, lst)
277
+ raise TypeError("reduce requires a callable")
278
+ def _builtin_push(self, lst, item):
279
+ lst.append(item)
280
+ return None
281
+ def _init_std_modules(self):
282
+ self.std_modules = {
283
+ 'math': {
284
+ 'sin': math.sin,
285
+ 'cos': math.cos,
286
+ 'tan': math.tan,
287
+ 'sqrt': math.sqrt,
288
+ 'floor': math.floor,
289
+ 'ceil': math.ceil,
290
+ 'abs': abs,
291
+ 'pow': pow,
292
+ 'log': math.log,
293
+ 'log10': math.log10,
294
+ 'exp': math.exp,
295
+ 'random': random.random,
296
+ 'randint': random.randint,
297
+ 'pi': math.pi,
298
+ 'e': math.e,
299
+ },
300
+ 'time': {
301
+ 'time': time.time,
302
+ 'sleep': time.sleep,
303
+ 'date': lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
304
+ 'year': lambda: datetime.now().year,
305
+ 'month': lambda: datetime.now().month,
306
+ 'day': lambda: datetime.now().day,
307
+ 'hour': lambda: datetime.now().hour,
308
+ 'minute': lambda: datetime.now().minute,
309
+ 'second': lambda: datetime.now().second,
310
+ },
311
+ 'http': {
312
+ 'get': self._http_get,
313
+ 'post': self._http_post
314
+ },
315
+ 'env': {
316
+ 'get': lambda k, d=None: os.environ.get(k, d),
317
+ 'set': lambda k, v: os.environ.__setitem__(k, str(v)),
318
+ 'all': lambda: dict(os.environ),
319
+ 'has': lambda k: k in os.environ,
320
+ },
321
+ 'args': {
322
+ 'get': lambda i: sys.argv[i+1] if i+1 < len(sys.argv) else None,
323
+ 'all': lambda: sys.argv[1:],
324
+ 'count': lambda: len(sys.argv) - 1,
325
+ },
326
+ 'path': {
327
+ 'join': os.path.join,
328
+ 'basename': os.path.basename,
329
+ 'dirname': os.path.dirname,
330
+ 'exists': os.path.exists,
331
+ 'isfile': os.path.isfile,
332
+ 'isdir': os.path.isdir,
333
+ 'abspath': os.path.abspath,
334
+ 'split': os.path.split,
335
+ 'ext': lambda p: os.path.splitext(p)[1],
336
+ },
337
+ 'color': {
338
+ 'red': lambda s: f"\033[91m{s}\033[0m",
339
+ 'green': lambda s: f"\033[92m{s}\033[0m",
340
+ 'yellow': lambda s: f"\033[93m{s}\033[0m",
341
+ 'blue': lambda s: f"\033[94m{s}\033[0m",
342
+ 'magenta': lambda s: f"\033[95m{s}\033[0m",
343
+ 'cyan': lambda s: f"\033[96m{s}\033[0m",
344
+ 'bold': lambda s: f"\033[1m{s}\033[0m",
345
+ 'underline': lambda s: f"\033[4m{s}\033[0m",
346
+ 'reset': "\033[0m",
347
+ },
348
+ 're': {
349
+ 'match': lambda p, s: bool(re.match(p, s)),
350
+ 'search': lambda p, s: re.search(p, s).group() if re.search(p, s) else None,
351
+ 'replace': lambda p, r, s: re.sub(p, r, s),
352
+ 'findall': lambda p, s: re.findall(p, s),
353
+ 'split': lambda p, s: re.split(p, s),
354
+ },
355
+ }
356
+ def _http_get(self, url):
357
+ with urllib.request.urlopen(url) as response:
358
+ return response.read().decode('utf-8')
359
+ def _http_post(self, url, data):
360
+ if isinstance(data, str):
361
+ json_data = data.encode('utf-8')
362
+ else:
363
+ json_data = json.dumps(data).encode('utf-8')
364
+ req = urllib.request.Request(url, data=json_data, headers={'Content-Type': 'application/json'})
365
+ with urllib.request.urlopen(req) as response:
366
+ return response.read().decode('utf-8')
367
+ def visit(self, node: Node) -> Any:
368
+ try:
369
+ method_name = f'visit_{type(node).__name__}'
370
+ visitor = getattr(self, method_name, self.generic_visit)
371
+ return visitor(node)
372
+ except ReturnException:
373
+ raise
374
+ except Exception as e:
375
+ if not hasattr(e, 'line') and hasattr(node, 'line'):
376
+ e.line = node.line
377
+ raise e
378
+ def generic_visit(self, node: Node):
379
+ raise Exception(f'No visit_{type(node).__name__} method')
380
+ def visit_Number(self, node: Number):
381
+ return node.value
382
+ def visit_String(self, node: String):
383
+ return node.value
384
+ def visit_Boolean(self, node: Boolean):
385
+ return node.value
386
+ def visit_ListVal(self, node: ListVal):
387
+ result = []
388
+ for e in node.elements:
389
+ if isinstance(e, Spread):
390
+ spread_val = self.visit(e.value)
391
+ if not isinstance(spread_val, list):
392
+ raise TypeError(f"Spread operator requires a list, got {type(spread_val).__name__}")
393
+ result.extend(spread_val)
394
+ else:
395
+ result.append(self.visit(e))
396
+ return result
397
+ def visit_Dictionary(self, node: Dictionary):
398
+ return {self.visit(k): self.visit(v) for k, v in node.pairs}
399
+ def visit_PropertyAssign(self, node: PropertyAssign):
400
+ instance = self.current_env.get(node.instance_name)
401
+ val = self.visit(node.value)
402
+ if isinstance(instance, Instance):
403
+ instance.data[node.property_name] = val
404
+ return val
405
+ elif isinstance(instance, dict):
406
+ instance[node.property_name] = val
407
+ return val
408
+ else:
409
+ raise TypeError(f"Cannot assign property '{node.property_name}' of non-object '{node.instance_name}'")
410
+ def visit_VarAccess(self, node: VarAccess):
411
+ try:
412
+ return self.current_env.get(node.name)
413
+ except NameError:
414
+ if node.name in self.builtins:
415
+ val = self.builtins[node.name]
416
+ if node.name in ('random', 'time_now', 'date_str'):
417
+ return val()
418
+ return val
419
+ if node.name in self.functions:
420
+ return self.visit_Call(Call(node.name, []))
421
+ raise
422
+ def visit_Assign(self, node: Assign):
423
+ value = self.visit(node.value)
424
+ self.current_env.set(node.name, value)
425
+ return value
426
+ def visit_BinOp(self, node: BinOp):
427
+ left = self.visit(node.left)
428
+ right = self.visit(node.right)
429
+ if node.op == '+':
430
+ if isinstance(left, str) or isinstance(right, str):
431
+ return str(left) + str(right)
432
+ if isinstance(left, list) and isinstance(right, list):
433
+ return left + right
434
+ return left + right
435
+ elif node.op == '-':
436
+ return left - right
437
+ elif node.op == '*':
438
+ return left * right
439
+ elif node.op == '/':
440
+ return left / right
441
+ elif node.op == '%':
442
+ return left % right
443
+ elif node.op == '==':
444
+ return left == right
445
+ elif node.op == '!=':
446
+ return left != right
447
+ elif node.op == '<':
448
+ return left < right
449
+ elif node.op == '>':
450
+ return left > right
451
+ elif node.op == '<=':
452
+ return left <= right
453
+ elif node.op == '>=':
454
+ return left >= right
455
+ elif node.op == 'and':
456
+ return left and right
457
+ elif node.op == 'or':
458
+ return left or right
459
+ elif node.op == 'matches':
460
+ pattern = right
461
+ if hasattr(pattern, 'search'):
462
+ return bool(pattern.search(str(left)))
463
+ return bool(re.search(str(pattern), str(left)))
464
+ raise Exception(f"Unknown operator: {node.op}")
465
+ def visit_Print(self, node: Print):
466
+ value = self.visit(node.expression)
467
+ if node.color or node.style:
468
+ colors = {
469
+ 'red': '91', 'green': '92', 'yellow': '93', 'blue': '94',
470
+ 'magenta': '95', 'cyan': '96'
471
+ }
472
+ code_parts = []
473
+ if node.style == 'bold':
474
+ code_parts.append('1')
475
+ if node.color and node.color.lower() in colors:
476
+ code_parts.append(colors[node.color.lower()])
477
+ if code_parts:
478
+ ansi_code = "\033[" + ";".join(code_parts) + "m"
479
+ print(f"{ansi_code}{value}\033[0m")
480
+ return value
481
+ print(value)
482
+ return value
483
+ def visit_If(self, node: If):
484
+ condition = self.visit(node.condition)
485
+ if condition:
486
+ for stmt in node.body:
487
+ self.visit(stmt)
488
+ elif node.else_body:
489
+ for stmt in node.else_body:
490
+ self.visit(stmt)
491
+ def visit_For(self, node: For):
492
+ count = self.visit(node.count)
493
+ if not isinstance(count, int):
494
+ raise TypeError(f"Loop count must be an integer, got {type(count)}")
495
+ for _ in range(count):
496
+ try:
497
+ for stmt in node.body:
498
+ self.visit(stmt)
499
+ except StopException:
500
+ break
501
+ except SkipException:
502
+ continue
503
+ except ReturnException:
504
+ raise
505
+ def visit_Input(self, node: Input):
506
+ if node.prompt:
507
+ return input(node.prompt)
508
+ return input()
509
+ def visit_While(self, node: While):
510
+ while self.visit(node.condition):
511
+ try:
512
+ for stmt in node.body:
513
+ self.visit(stmt)
514
+ except StopException:
515
+ break
516
+ except SkipException:
517
+ continue
518
+ except ReturnException:
519
+ raise
520
+ def visit_Try(self, node: Try):
521
+ try:
522
+ for stmt in node.try_body:
523
+ self.visit(stmt)
524
+ except Exception as e:
525
+ error_msg = str(e)
526
+ if hasattr(e, 'message'):
527
+ error_msg = e.message
528
+ self.current_env.set(node.catch_var, error_msg)
529
+ for stmt in node.catch_body:
530
+ self.visit(stmt)
531
+ def visit_TryAlways(self, node: TryAlways):
532
+ try:
533
+ try:
534
+ for stmt in node.try_body:
535
+ self.visit(stmt)
536
+ except Exception as e:
537
+ error_msg = str(e)
538
+ if hasattr(e, 'message'):
539
+ error_msg = e.message
540
+ self.current_env.set(node.catch_var, error_msg)
541
+ for stmt in node.catch_body:
542
+ self.visit(stmt)
543
+ finally:
544
+ for stmt in node.always_body:
545
+ self.visit(stmt)
546
+ def visit_UnaryOp(self, node: UnaryOp):
547
+ val = self.visit(node.right)
548
+ if node.op == 'not':
549
+ return not val
550
+ raise Exception(f"Unknown unary operator: {node.op}")
551
+ def visit_FunctionDef(self, node: FunctionDef):
552
+ self.functions[node.name] = node
553
+ def visit_Return(self, node: Return):
554
+ value = self.visit(node.value)
555
+ raise ReturnException(value)
556
+ def _call_function_def(self, func_def: FunctionDef, args: List[Node]):
557
+ if len(args) > len(func_def.args):
558
+ raise TypeError(f"Function '{func_def.name}' expects max {len(func_def.args)} arguments, got {len(args)}")
559
+ old_env = self.current_env
560
+ new_env = Environment(parent=self.global_env)
561
+ for i, (arg_name, default_node, type_hint) in enumerate(func_def.args):
562
+ if i < len(args):
563
+ val = self.visit(args[i])
564
+ elif default_node is not None:
565
+ val = self.visit(default_node)
566
+ else:
567
+ raise TypeError(f"Missing required argument '{arg_name}' for function '{func_def.name}'")
568
+ if type_hint:
569
+ self._check_type(arg_name, val, type_hint)
570
+ new_env.set(arg_name, val)
571
+ self.current_env = new_env
572
+ ret_val = None
573
+ try:
574
+ for stmt in func_def.body:
575
+ val = self.visit(stmt)
576
+ ret_val = val
577
+ except ReturnException as e:
578
+ ret_val = e.value
579
+ finally:
580
+ self.current_env = old_env
581
+ return ret_val
582
+ def visit_Call(self, node: Call):
583
+ if node.name in self.builtins:
584
+ args = [self.visit(a) for a in node.args]
585
+ result = self.builtins[node.name](*args)
586
+ if isinstance(result, Tag):
587
+ if node.body:
588
+ self.web.push(result)
589
+ try:
590
+ for stmt in node.body:
591
+ res = self.visit(stmt)
592
+ if res is not None and (isinstance(res, str) or isinstance(res, Tag)):
593
+ self.web.add_text(res)
594
+ finally:
595
+ self.web.pop()
596
+ return result
597
+ return result
598
+ try:
599
+ func = self.current_env.get(node.name)
600
+ if callable(func):
601
+ args = [self.visit(a) for a in node.args]
602
+ return func(*args)
603
+ curr_obj = func
604
+ if (isinstance(curr_obj, (list, dict, str)) or isinstance(curr_obj, Instance)):
605
+ valid_chain = True
606
+ for arg_node in node.args:
607
+ val = self.visit(arg_node)
608
+ if isinstance(val, list) and len(val) == 1:
609
+ idx = val[0]
610
+ try:
611
+ curr_obj = curr_obj[idx]
612
+ except (IndexError, KeyError) as e:
613
+ raise RuntimeError(f"Index/Key error: {e}")
614
+ except TypeError:
615
+ valid_chain = False; break
616
+ else:
617
+ valid_chain = False
618
+ break
619
+ if valid_chain:
620
+ return curr_obj
621
+ pass
622
+ except NameError:
623
+ pass
624
+ except NameError:
625
+ pass
626
+ if node.name not in self.functions:
627
+ raise NameError(f"Function '{node.name}' not defined (and not a variable).")
628
+ func_def = self.functions[node.name]
629
+ return self._call_function_def(func_def, node.args)
630
+ def visit_ClassDef(self, node: ClassDef):
631
+ self.classes[node.name] = node
632
+ def visit_Instantiation(self, node: Instantiation):
633
+ if node.class_name not in self.classes:
634
+ raise NameError(f"Class '{node.class_name}' not defined.")
635
+ class_def = self.classes[node.class_name]
636
+ all_properties = self._get_class_properties(class_def)
637
+
638
+ # Check args length
639
+ # First count how many properties are required (no default)
640
+ required_count = 0
641
+ for name, default_val in all_properties:
642
+ if default_val is None:
643
+ required_count += 1
644
+
645
+ if len(node.args) < required_count:
646
+ raise TypeError(f"Structure '{node.class_name}' expects at least {required_count} args, got {len(node.args)}")
647
+
648
+ instance = Instance(class_def)
649
+
650
+ for i, (prop_name, default_val) in enumerate(all_properties):
651
+ val = None
652
+ if i < len(node.args):
653
+ val = self.visit(node.args[i])
654
+ elif default_val is not None:
655
+ val = self.visit(default_val)
656
+ else:
657
+ # Should be caught by required_count check, but safety fallback
658
+ raise TypeError(f"Missing argument for property '{prop_name}' in '{node.class_name}'")
659
+
660
+ instance.data[prop_name] = val
661
+
662
+ self.current_env.set(node.var_name, instance)
663
+ return instance
664
+ def visit_MethodCall(self, node: MethodCall):
665
+ instance = self.current_env.get(node.instance_name)
666
+ if isinstance(instance, dict):
667
+ if node.method_name not in instance:
668
+ raise AttributeError(f"Module '{node.instance_name}' has no method '{node.method_name}'")
669
+ method = instance[node.method_name]
670
+ if isinstance(method, FunctionDef):
671
+ return self._call_function_def(method, node.args)
672
+ elif callable(method):
673
+ args = [self.visit(a) for a in node.args]
674
+ try:
675
+ return method(*args)
676
+ except Exception as e:
677
+ raise RuntimeError(f"Error calling '{node.instance_name}.{node.method_name}': {e}")
678
+ elif isinstance(method, (dict, list, str)):
679
+ curr_obj = method
680
+ valid_chain = True
681
+ for arg_node in node.args:
682
+ val = self.visit(arg_node)
683
+ if isinstance(val, list) and len(val) == 1:
684
+ idx = val[0]
685
+ try:
686
+ curr_obj = curr_obj[idx]
687
+ except (IndexError, KeyError) as e:
688
+ raise RuntimeError(f"Index/Key error: {e}")
689
+ except TypeError:
690
+ valid_chain = False; break
691
+ else:
692
+ valid_chain = False; break
693
+ if valid_chain:
694
+ return curr_obj
695
+ raise TypeError(f"Property '{node.method_name}' is not callable and index access failed.")
696
+ else:
697
+ raise TypeError(f"Property '{node.method_name}' is not callable.")
698
+ if hasattr(instance, node.method_name) and callable(getattr(instance, node.method_name)):
699
+ method = getattr(instance, node.method_name)
700
+ args = [self.visit(a) for a in node.args]
701
+ return method(*args)
702
+ if not isinstance(instance, Instance):
703
+ raise TypeError(f"'{node.instance_name}' is not a structure instance (and has no native method '{node.method_name}').")
704
+ method_node = self._find_method(instance.class_def, node.method_name)
705
+ if not method_node:
706
+ raise AttributeError(f"Structure '{instance.class_def.name}' has no method '{node.method_name}'")
707
+ old_env = self.current_env
708
+ new_env = Environment(parent=self.global_env)
709
+ for k, v in instance.data.items():
710
+ new_env.set(k, v)
711
+ if len(node.args) > len(method_node.args):
712
+ raise TypeError(f"Method '{node.method_name}' expects max {len(method_node.args)} arguments.")
713
+ for i, (arg_name, default_node, type_hint) in enumerate(method_node.args):
714
+ if i < len(node.args):
715
+ val = self.visit(node.args[i])
716
+ elif default_node is not None:
717
+ val = self.visit(default_node)
718
+ else:
719
+ raise TypeError(f"Missing required argument '{arg_name}' for method '{node.method_name}'")
720
+ new_env.set(arg_name, val)
721
+ self.current_env = new_env
722
+ ret_val = None
723
+ try:
724
+ for stmt in method_node.body:
725
+ self.visit(stmt)
726
+ except ReturnException as e:
727
+ ret_val = e.value
728
+ finally:
729
+ for k in instance.data.keys():
730
+ if k in new_env.variables:
731
+ instance.data[k] = new_env.variables[k]
732
+ self.current_env = old_env
733
+ return ret_val
734
+ def visit_PropertyAccess(self, node: PropertyAccess):
735
+ instance = self.current_env.get(node.instance_name)
736
+
737
+ # 1. ShellLite Instance
738
+ if isinstance(instance, Instance):
739
+ if node.property_name not in instance.data:
740
+ # Check for methods? PropertyAccess usually implies data.
741
+ # But in some cases we might want method reference?
742
+ raise AttributeError(f"Structure '{instance.class_def.name}' has no property '{node.property_name}'")
743
+ return instance.data[node.property_name]
744
+
745
+ # 2. Dictionary
746
+ elif isinstance(instance, dict):
747
+ if node.property_name in instance:
748
+ return instance[node.property_name]
749
+ raise AttributeError(f"Dictionary has no key '{node.property_name}'")
750
+
751
+ # 3. List
752
+ elif isinstance(instance, list):
753
+ if node.property_name == 'length':
754
+ return len(instance)
755
+
756
+ # 4. String
757
+ elif isinstance(instance, str):
758
+ if node.property_name == 'length':
759
+ return len(instance)
760
+
761
+ # 5. Python Object / Module Interop
762
+ # If the instance has the attribute natively, return it.
763
+ # This handles 'math.pi', 'os.name', etc.
764
+ if hasattr(instance, node.property_name):
765
+ return getattr(instance, node.property_name)
766
+
767
+ raise TypeError(f"Object '{node.instance_name}' (type {type(instance).__name__}) has no property '{node.property_name}'")
768
+
769
+ def visit_Import(self, node: Import):
770
+ if node.path in self.std_modules:
771
+ self.current_env.set(node.path, self.std_modules[node.path])
772
+ return
773
+ import os
774
+ if os.path.exists(node.path):
775
+ target_path = node.path
776
+ else:
777
+ home = os.path.expanduser("~")
778
+ global_path = os.path.join(home, ".shell_lite", "modules", node.path)
779
+ if os.path.exists(global_path):
780
+ target_path = global_path
781
+ else:
782
+ if not node.path.endswith('.shl'):
783
+ global_path_ext = global_path + ".shl"
784
+ if os.path.exists(global_path_ext):
785
+ target_path = global_path_ext
786
+ else:
787
+ raise FileNotFoundError(f"Could not find imported file: {node.path} (searched local and global modules)")
788
+ else:
789
+ raise FileNotFoundError(f"Could not find imported file: {node.path} (searched local and global modules)")
790
+ if os.path.isdir(target_path):
791
+ main_shl = os.path.join(target_path, "main.shl")
792
+ pkg_shl = os.path.join(target_path, f"{os.path.basename(target_path)}.shl")
793
+ if os.path.exists(main_shl):
794
+ target_path = main_shl
795
+ elif os.path.exists(pkg_shl):
796
+ target_path = pkg_shl
797
+ else:
798
+ raise FileNotFoundError(f"Package '{node.path}' is a folder but has no 'main.shl' or '{os.path.basename(target_path)}.shl'.")
799
+ try:
800
+ with open(target_path, 'r', encoding='utf-8') as f:
801
+ code = f.read()
802
+ except FileNotFoundError:
803
+ raise FileNotFoundError(f"Could not find imported file: {node.path}")
804
+ from .lexer import Lexer
805
+ from .parser import Parser
806
+ lexer = Lexer(code)
807
+ tokens = lexer.tokenize()
808
+ parser = Parser(tokens)
809
+ statements = parser.parse()
810
+ for stmt in statements:
811
+ self.visit(stmt)
812
+ def _get_class_properties(self, class_def: ClassDef) -> List[tuple[str, Optional[Node]]]:
813
+ if not hasattr(class_def, 'properties'): return []
814
+ # Support both old string list and new tuple list for backward compat if needed, though we updated AST
815
+ props = []
816
+ for p in class_def.properties:
817
+ if isinstance(p, tuple):
818
+ props.append(p)
819
+ else:
820
+ props.append((p, None))
821
+
822
+ if class_def.parent:
823
+ if class_def.parent not in self.classes:
824
+ raise NameError(f"Parent class '{class_def.parent}' not defined.")
825
+ parent_def = self.classes[class_def.parent]
826
+ return self._get_class_properties(parent_def) + props
827
+ return props
828
+ def _find_method(self, class_def: ClassDef, method_name: str) -> Optional[FunctionDef]:
829
+ for m in class_def.methods:
830
+ if m.name == method_name:
831
+ return m
832
+ if class_def.parent:
833
+ if class_def.parent not in self.classes:
834
+ raise NameError(f"Parent class '{class_def.parent}' not defined.")
835
+ parent_def = self.classes[class_def.parent]
836
+ return self._find_method(parent_def, method_name)
837
+ return None
838
+ def builtin_run(self, cmd):
839
+ try:
840
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
841
+ if result.returncode != 0:
842
+ print(f"Command Error: {result.stderr}")
843
+ return result.stdout.strip() or result.stderr.strip()
844
+ except Exception as e:
845
+ raise RuntimeError(f"Failed to run command: {e}")
846
+ def builtin_read(self, path):
847
+ try:
848
+ with open(path, 'r', encoding='utf-8') as f:
849
+ return f.read()
850
+ except Exception as e:
851
+ raise RuntimeError(f"Failed to read file '{path}': {e}")
852
+ def builtin_write(self, path, content):
853
+ try:
854
+ with open(path, 'w', encoding='utf-8') as f:
855
+ f.write(str(content))
856
+ return True
857
+ except Exception as e:
858
+ raise RuntimeError(f"Failed to write file '{path}': {e}")
859
+ def builtin_json_parse(self, json_str):
860
+ try:
861
+ return json.loads(json_str)
862
+ except Exception as e:
863
+ raise RuntimeError(f"Invalid JSON: {e}")
864
+ def builtin_json_stringify(self, obj):
865
+ try:
866
+ if isinstance(obj, Instance):
867
+ return json.dumps(obj.data)
868
+ return json.dumps(obj)
869
+ except Exception as e:
870
+ raise RuntimeError(f"JSON stringify failed: {e}")
871
+ def builtin_http_get(self, url):
872
+ try:
873
+ with urllib.request.urlopen(url) as response:
874
+ return response.read().decode('utf-8')
875
+ except Exception as e:
876
+ raise RuntimeError(f"HTTP GET failed for '{url}': {e}")
877
+ def builtin_http_post(self, url, data_dict):
878
+ try:
879
+ if isinstance(data_dict, Instance):
880
+ data_dict = data_dict.data
881
+ data = json.dumps(data_dict).encode('utf-8')
882
+ req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
883
+ with urllib.request.urlopen(req) as response:
884
+ return response.read().decode('utf-8')
885
+ except Exception as e:
886
+ raise RuntimeError(f"HTTP POST failed for '{url}': {e}")
887
+ def visit_Lambda(self, node: Lambda):
888
+ return LambdaFunction(node.params, node.body, self)
889
+ def visit_Ternary(self, node: Ternary):
890
+ condition = self.visit(node.condition)
891
+ if condition:
892
+ return self.visit(node.true_expr)
893
+ else:
894
+ return self.visit(node.false_expr)
895
+ def visit_ListComprehension(self, node: ListComprehension):
896
+ iterable = self.visit(node.iterable)
897
+ if not hasattr(iterable, '__iter__'):
898
+ raise TypeError(f"Cannot iterate over {type(iterable).__name__}")
899
+ result = []
900
+ old_env = self.current_env
901
+ new_env = Environment(parent=self.current_env)
902
+ self.current_env = new_env
903
+ try:
904
+ for item in iterable:
905
+ new_env.set(node.var_name, item)
906
+ if node.condition:
907
+ if not self.visit(node.condition):
908
+ continue
909
+ result.append(self.visit(node.expr))
910
+ finally:
911
+ self.current_env = old_env
912
+ return result
913
+ def visit_Spread(self, node: Spread):
914
+ return self.visit(node.value)
915
+ def visit_Alert(self, node: Alert):
916
+ msg = self.visit(node.message)
917
+ root = tk.Tk()
918
+ root.withdraw()
919
+ root.attributes('-topmost', True)
920
+ messagebox.showinfo("Alert", str(msg))
921
+ root.destroy()
922
+ def visit_Prompt(self, node: Prompt):
923
+ prompt = self.visit(node.prompt)
924
+ root = tk.Tk()
925
+ root.withdraw()
926
+ root.attributes('-topmost', True)
927
+ val = simpledialog.askstring("Input", str(prompt))
928
+ root.destroy()
929
+ return val if val is not None else ""
930
+ def visit_Confirm(self, node: Confirm):
931
+ prompt = self.visit(node.prompt)
932
+ root = tk.Tk()
933
+ root.withdraw()
934
+ root.attributes('-topmost', True)
935
+ val = messagebox.askyesno("Confirm", str(prompt))
936
+ root.destroy()
937
+ return val
938
+ def visit_Spawn(self, node: Spawn):
939
+ executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
940
+ future = executor.submit(self.visit, node.call)
941
+ return future
942
+ def visit_Await(self, node: Await):
943
+ task = self.visit(node.task)
944
+ if isinstance(task, concurrent.futures.Future):
945
+ return task.result()
946
+ raise TypeError(f"Cannot await non-task object: {type(task)}")
947
+ def visit_Regex(self, node: Regex):
948
+ return re.compile(node.pattern)
949
+ def visit_FileWatcher(self, node: FileWatcher):
950
+ path = self.visit(node.path)
951
+ if not os.path.exists(path):
952
+ print(f"Warning: Watching non-existent file {path}")
953
+ last_mtime = 0
954
+ else:
955
+ last_mtime = os.path.getmtime(path)
956
+ try:
957
+ while True:
958
+ current_exists = os.path.exists(path)
959
+ if current_exists:
960
+ current_mtime = os.path.getmtime(path)
961
+ if current_mtime != last_mtime:
962
+ last_mtime = current_mtime
963
+ for stmt in node.body:
964
+ self.visit(stmt)
965
+ time.sleep(1)
966
+ except StopException:
967
+ pass
968
+ except ReturnException:
969
+ raise
970
+ def _check_type(self, arg_name, val, type_hint):
971
+ if type_hint == 'int' and not isinstance(val, int):
972
+ raise TypeError(f"Argument '{arg_name}' expects int, got {type(val).__name__}")
973
+ elif type_hint == 'str' and not isinstance(val, str):
974
+ raise TypeError(f"Argument '{arg_name}' expects str, got {type(val).__name__}")
975
+ elif type_hint == 'bool' and not isinstance(val, bool):
976
+ raise TypeError(f"Argument '{arg_name}' expects bool, got {type(val).__name__}")
977
+ elif type_hint == 'float' and not isinstance(val, (float, int)):
978
+ raise TypeError(f"Argument '{arg_name}' expects float, got {type(val).__name__}")
979
+ elif type_hint == 'list' and not isinstance(val, list):
980
+ raise TypeError(f"Argument '{arg_name}' expects list, got {type(val).__name__}")
981
+ def visit_ConstAssign(self, node: ConstAssign):
982
+ value = self.visit(node.value)
983
+ self.current_env.set_const(node.name, value)
984
+ return value
985
+ def visit_ForIn(self, node: ForIn):
986
+ iterable = self.visit(node.iterable)
987
+ if not hasattr(iterable, '__iter__'):
988
+ raise TypeError(f"Cannot iterate over {type(iterable).__name__}")
989
+ old_env = self.current_env
990
+ new_env = Environment(parent=self.current_env)
991
+ self.current_env = new_env
992
+ try:
993
+ for item in iterable:
994
+ new_env.set(node.var_name, item)
995
+ for stmt in node.body:
996
+ self.visit(stmt)
997
+ except ReturnException:
998
+ raise
999
+ finally:
1000
+ self.current_env = old_env
1001
+ def visit_IndexAccess(self, node: IndexAccess):
1002
+ obj = self.visit(node.obj)
1003
+ index = self.visit(node.index)
1004
+ if isinstance(obj, list):
1005
+ if not isinstance(index, int):
1006
+ raise TypeError(f"List indices must be integers, got {type(index).__name__}")
1007
+ return obj[index]
1008
+ elif isinstance(obj, dict):
1009
+ return obj[index]
1010
+ elif isinstance(obj, str):
1011
+ if not isinstance(index, int):
1012
+ raise TypeError(f"String indices must be integers, got {type(index).__name__}")
1013
+ return obj[index]
1014
+ else:
1015
+ raise TypeError(f"'{type(obj).__name__}' object is not subscriptable")
1016
+ def visit_Stop(self, node: Stop):
1017
+ raise StopException()
1018
+ def visit_Skip(self, node: Skip):
1019
+ raise SkipException()
1020
+ def visit_PythonImport(self, node: PythonImport):
1021
+ try:
1022
+ mod = importlib.import_module(node.module_name)
1023
+ name = node.alias if node.alias else node.module_name.split('.')[0]
1024
+ self.global_env.set(name, mod)
1025
+ except ImportError as e:
1026
+ raise RuntimeError(f"Could not import python module '{node.module_name}': {e}")
1027
+
1028
+ def visit_Throw(self, node: Throw):
1029
+ message = self.visit(node.message)
1030
+ raise ShellLiteError(str(message))
1031
+ def visit_Unless(self, node: Unless):
1032
+ condition = self.visit(node.condition)
1033
+ if not condition:
1034
+ for stmt in node.body:
1035
+ self.visit(stmt)
1036
+ elif node.else_body:
1037
+ for stmt in node.else_body:
1038
+ self.visit(stmt)
1039
+ def visit_Until(self, node: Until):
1040
+ while not self.visit(node.condition):
1041
+ try:
1042
+ for stmt in node.body:
1043
+ self.visit(stmt)
1044
+ except StopException:
1045
+ break
1046
+ except SkipException:
1047
+ continue
1048
+ except ReturnException:
1049
+ raise
1050
+ def visit_Repeat(self, node: Repeat):
1051
+ count = self.visit(node.count)
1052
+ if not isinstance(count, int):
1053
+ raise TypeError(f"repeat count must be an integer, got {type(count).__name__}")
1054
+ for _ in range(count):
1055
+ try:
1056
+ for stmt in node.body:
1057
+ self.visit(stmt)
1058
+ except StopException:
1059
+ break
1060
+ except SkipException:
1061
+ continue
1062
+ except ReturnException:
1063
+ raise
1064
+ def visit_When(self, node: When):
1065
+ value = self.visit(node.value)
1066
+ for match_val, body in node.cases:
1067
+ if self.visit(match_val) == value:
1068
+ for stmt in body:
1069
+ self.visit(stmt)
1070
+ return
1071
+ if node.otherwise:
1072
+ for stmt in node.otherwise:
1073
+ self.visit(stmt)
1074
+ def visit_Execute(self, node: Execute):
1075
+ code = self.visit(node.code)
1076
+ if not isinstance(code, str):
1077
+ raise TypeError(f"execute requires a string, got {type(code).__name__}")
1078
+ from .lexer import Lexer
1079
+ from .parser import Parser
1080
+ lexer = Lexer(code)
1081
+ tokens = lexer.tokenize()
1082
+ parser = Parser(tokens)
1083
+ statements = parser.parse()
1084
+ result = None
1085
+ for stmt in statements:
1086
+ result = self.visit(stmt)
1087
+ self.current_env.set('__exec_result__', result)
1088
+ return result
1089
+ def visit_ImportAs(self, node: ImportAs):
1090
+ if node.path in self.std_modules:
1091
+ self.current_env.set(node.alias, self.std_modules[node.path])
1092
+ return
1093
+ old_funcs_keys = set(self.functions.keys())
1094
+ module_env = Environment(parent=self.global_env)
1095
+ old_env = self.current_env
1096
+ self.current_env = module_env
1097
+ module_env = Environment(parent=self.global_env)
1098
+ old_env = self.current_env
1099
+ self.current_env = module_env
1100
+ if os.path.exists(node.path):
1101
+ target_path = node.path
1102
+ else:
1103
+ home = os.path.expanduser("~")
1104
+ global_path = os.path.join(home, ".shell_lite", "modules", node.path)
1105
+ if os.path.exists(global_path):
1106
+ target_path = global_path
1107
+ else:
1108
+ if not node.path.endswith('.shl'):
1109
+ global_path_ext = global_path + ".shl"
1110
+ if os.path.exists(global_path_ext):
1111
+ target_path = global_path_ext
1112
+ else:
1113
+ self.current_env = old_env
1114
+ raise FileNotFoundError(f"Could not find imported file: {node.path} (searched local and global modules)")
1115
+ else:
1116
+ self.current_env = old_env
1117
+ raise FileNotFoundError(f"Could not find imported file: {node.path} (searched local and global modules)")
1118
+ if os.path.isdir(target_path):
1119
+ main_shl = os.path.join(target_path, "main.shl")
1120
+ pkg_shl = os.path.join(target_path, f"{os.path.basename(target_path)}.shl")
1121
+ if os.path.exists(main_shl):
1122
+ target_path = main_shl
1123
+ elif os.path.exists(pkg_shl):
1124
+ target_path = pkg_shl
1125
+ else:
1126
+ self.current_env = old_env
1127
+ raise FileNotFoundError(f"Package '{node.path}' is a folder but has no 'main.shl' or '{os.path.basename(target_path)}.shl'.")
1128
+ try:
1129
+ with open(target_path, 'r', encoding='utf-8') as f:
1130
+ code = f.read()
1131
+ from .lexer import Lexer
1132
+ from .parser import Parser
1133
+ lexer = Lexer(code)
1134
+ tokens = lexer.tokenize()
1135
+ parser = Parser(tokens)
1136
+ statements = parser.parse()
1137
+ for stmt in statements:
1138
+ self.visit(stmt)
1139
+ module_exports = {}
1140
+ module_exports.update(module_env.variables)
1141
+ current_funcs_keys = set(self.functions.keys())
1142
+ new_funcs = current_funcs_keys - old_funcs_keys
1143
+ for fname in new_funcs:
1144
+ func_node = self.functions[fname]
1145
+ module_exports[fname] = func_node
1146
+ del self.functions[fname]
1147
+ self.current_env = old_env
1148
+ self.current_env.set(node.alias, module_exports)
1149
+ except Exception as e:
1150
+ self.current_env = old_env
1151
+ raise RuntimeError(f"Failed to import '{node.path}': {e}")
1152
+ def visit_Forever(self, node: Forever):
1153
+ while True:
1154
+ try:
1155
+ for stmt in node.body:
1156
+ self.visit(stmt)
1157
+ except StopException:
1158
+ break
1159
+ except SkipException:
1160
+ continue
1161
+ except ReturnException:
1162
+ raise
1163
+ def visit_Exit(self, node: Exit):
1164
+ code = 0
1165
+ if node.code:
1166
+ code = self.visit(node.code)
1167
+ sys.exit(int(code))
1168
+ sys.exit(0)
1169
+
1170
+ # -------------------------------------------------------------------------
1171
+ # Project Polaris: Phase 2 (The Canvas - Native UI)
1172
+ # -------------------------------------------------------------------------
1173
+ def visit_App(self, node: App):
1174
+ # We need a root for the app
1175
+ import tkinter as tk
1176
+ from tkinter import messagebox
1177
+ root = tk.Tk()
1178
+ root.title(node.title)
1179
+ root.geometry(f"{node.width}x{node.height}")
1180
+
1181
+ # Store root for potential access, though mostly we use 'master' passed down
1182
+ # Ideally we pass 'parent' to visits, but we don't have that signature.
1183
+ # So we'll use a stack or a temporary context.
1184
+ self.ui_parent_stack = [root]
1185
+
1186
+ # Define a helpful alert function available in UI context
1187
+ def ui_alert(msg):
1188
+ messagebox.showinfo("Message", str(msg))
1189
+ self.current_env.set("alert", ui_alert)
1190
+
1191
+ try:
1192
+ for child in node.body:
1193
+ self.visit(child)
1194
+ finally:
1195
+ self.ui_parent_stack.pop()
1196
+
1197
+ root.mainloop()
1198
+
1199
+ def visit_Layout(self, node: Layout):
1200
+ parent = self.ui_parent_stack[-1]
1201
+
1202
+ # Create a frame for the layout
1203
+ frame = tk.Frame(parent)
1204
+
1205
+ # Pack options based on layout type of THIS container relative to parent??
1206
+ # Usually Layout implies how CHILDREN are arranged.
1207
+ # But here 'column' means "I am a column" -> children stacked vertically.
1208
+ # 'row' means "I am a row" -> children stacked horizontally.
1209
+
1210
+ # In Tkinter, pack() defaults to vertical (column).
1211
+ # side=LEFT makes it horizontal (row).
1212
+
1213
+ # We start by adding the frame to the parent.
1214
+ # If parent is a Column, we pack(side=TOP). If Row, pack(side=LEFT).
1215
+ # But simplified: Just use pack(fill=X) or something.
1216
+ frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
1217
+
1218
+ self.ui_parent_stack.append((frame, node.layout_type))
1219
+ try:
1220
+ for child in node.body:
1221
+ self.visit(child)
1222
+ finally:
1223
+ self.ui_parent_stack.pop()
1224
+
1225
+ def visit_Widget(self, node: Widget):
1226
+ from tkinter import messagebox
1227
+ parent_ctx = self.ui_parent_stack[-1]
1228
+ if isinstance(parent_ctx, tuple):
1229
+ parent, layout_mode = parent_ctx
1230
+ else:
1231
+ parent = parent_ctx
1232
+ layout_mode = 'column' # Default to column
1233
+
1234
+ widget = None
1235
+ if node.widget_type == 'button':
1236
+ # Handle event
1237
+ def on_click():
1238
+ if node.event_handler:
1239
+ try:
1240
+ for stmt in node.event_handler:
1241
+ self.visit(stmt)
1242
+ except Exception as e:
1243
+ messagebox.showerror("Error", str(e))
1244
+
1245
+ widget = tk.Button(parent, text=node.label, command=on_click)
1246
+
1247
+ elif node.widget_type == 'input':
1248
+ lbl = tk.Label(parent, text=node.label)
1249
+ pack_opts = {'side': tk.TOP, 'anchor': 'w'} if layout_mode == 'column' else {'side': tk.LEFT}
1250
+ lbl.pack(**pack_opts)
1251
+
1252
+ widget = tk.Entry(parent)
1253
+
1254
+ # Store accessor in Env so we can read .value
1255
+ if node.var_name:
1256
+ # We can't store the widget directly because .value access visits PropertyAccess
1257
+ # which expects dict, list, or python object.
1258
+ # Tkinter Entry has get().
1259
+ # We wrap it or just rely on 'visit_PropertyAccess' (Phase 1)
1260
+ # By default Tkinter widgets store config.
1261
+ # Let's verify if widget.value works natively? No.
1262
+ # So we wrap it.
1263
+ class InputWrapper:
1264
+ def __init__(self, w): self.w = w
1265
+ @property
1266
+ def value(self): return self.w.get()
1267
+ @property
1268
+ def text(self): return self.w.get()
1269
+
1270
+ self.current_env.set(node.var_name, InputWrapper(widget))
1271
+
1272
+ elif node.widget_type == 'heading':
1273
+ widget = tk.Label(parent, text=node.label, font=("Helvetica", 16, "bold"))
1274
+
1275
+ elif node.widget_type == 'text':
1276
+ widget = tk.Label(parent, text=node.label)
1277
+
1278
+ if widget:
1279
+ # Layout the widget
1280
+ if layout_mode == 'column':
1281
+ widget.pack(side=tk.TOP, pady=5, fill=tk.X)
1282
+ else:
1283
+ widget.pack(side=tk.LEFT, padx=5)
1284
+
1285
+ def visit_Make(self, node: Make):
1286
+ if node.class_name not in self.classes:
1287
+ raise NameError(f"Thing '{node.class_name}' not defined.")
1288
+ class_def = self.classes[node.class_name]
1289
+ props = self._get_class_properties(class_def)
1290
+
1291
+ # Check args length
1292
+ required_count = 0
1293
+ for name, default_val in props:
1294
+ if default_val is None:
1295
+ required_count += 1
1296
+
1297
+ if len(node.args) < required_count:
1298
+ raise TypeError(f"Thing '{node.class_name}' expects at least {required_count} values, got {len(node.args)}")
1299
+
1300
+ instance = Instance(class_def)
1301
+
1302
+ for i, (prop_name, default_val) in enumerate(props):
1303
+ val = None
1304
+ if i < len(node.args):
1305
+ val = self.visit(node.args[i])
1306
+ elif default_val is not None:
1307
+ val = self.visit(default_val)
1308
+ else:
1309
+ raise TypeError(f"Missing argument for property '{prop_name}' in '{node.class_name}'")
1310
+
1311
+ instance.data[prop_name] = val
1312
+
1313
+ return instance
1314
+ def visit_Convert(self, node: Convert):
1315
+ val = self.visit(node.expression)
1316
+ if node.target_format.lower() == 'json':
1317
+ if isinstance(val, str):
1318
+ try:
1319
+ return json.loads(val)
1320
+ except:
1321
+ return json.dumps(val)
1322
+ else:
1323
+ if isinstance(val, Instance):
1324
+ return json.dumps(val.data)
1325
+ return json.dumps(val)
1326
+ raise ValueError(f"Unknown conversion format: {node.target_format}")
1327
+ def visit_ProgressLoop(self, node: ProgressLoop):
1328
+ loop = node.loop_node
1329
+ if isinstance(loop, Repeat):
1330
+ count = self.visit(loop.count)
1331
+ if not isinstance(count, int): count = 0
1332
+ print(f"Progress: [ ] 0%", end='\r')
1333
+ for i in range(count):
1334
+ percent = int((i / count) * 100)
1335
+ bar = '=' * int(percent / 5)
1336
+ print(f"Progress: [{bar:<20}] {percent}%", end='\r')
1337
+ try:
1338
+ for stmt in loop.body:
1339
+ self.visit(stmt)
1340
+ except: pass
1341
+ print(f"Progress: [{'='*20}] 100% ")
1342
+ elif isinstance(loop, For):
1343
+ count = self.visit(loop.count)
1344
+ for i in range(count):
1345
+ percent = int((i / count) * 100)
1346
+ bar = '=' * int(percent / 5)
1347
+ print(f"Progress: [{bar:<20}] {percent}%", end='\r')
1348
+ try:
1349
+ for stmt in loop.body:
1350
+ self.visit(stmt)
1351
+ except: pass
1352
+ print(f"Progress: [{'='*20}] 100% ")
1353
+ elif isinstance(loop, ForIn):
1354
+ iterable = self.visit(loop.iterable)
1355
+ total = len(iterable) if hasattr(iterable, '__len__') else 0
1356
+ i = 0
1357
+ for item in iterable:
1358
+ if total > 0:
1359
+ percent = int((i / total) * 100)
1360
+ bar = '=' * int(percent / 5)
1361
+ print(f"Progress: [{bar:<20}] {percent}%", end='\r')
1362
+ self.current_env.set(loop.var_name, item)
1363
+ try:
1364
+ for stmt in loop.body:
1365
+ self.visit(stmt)
1366
+ except: pass
1367
+ i += 1
1368
+ if total > 0:
1369
+ print(f"Progress: [{'='*20}] 100% ")
1370
+ def visit_DatabaseOp(self, node: DatabaseOp):
1371
+ if node.op == 'open':
1372
+ path = self.visit(node.args[0])
1373
+ self.db_conn = sqlite3.connect(path, check_same_thread=False)
1374
+ return self.db_conn
1375
+ elif node.op == 'close':
1376
+ if self.db_conn:
1377
+ self.db_conn.close()
1378
+ self.db_conn = None
1379
+ elif node.op == 'exec':
1380
+ if not self.db_conn:
1381
+ raise RuntimeError("Database not open. Use 'db open \"path\"' first.")
1382
+ sql = self.visit(node.args[0])
1383
+ params = [self.visit(arg) for arg in node.args[1:]]
1384
+ cursor = self.db_conn.cursor()
1385
+ cursor.execute(sql, params)
1386
+ self.db_conn.commit()
1387
+ return cursor.lastrowid
1388
+ elif node.op == 'query':
1389
+ if not self.db_conn:
1390
+ raise RuntimeError("Database not open. Use 'db open \"path\"' first.")
1391
+ sql = self.visit(node.args[0])
1392
+ params = [self.visit(arg) for arg in node.args[1:]]
1393
+ cursor = self.db_conn.cursor()
1394
+ cursor.execute(sql, params)
1395
+ columns = [description[0] for description in cursor.description] if cursor.description else []
1396
+ rows = cursor.fetchall()
1397
+ result = []
1398
+ for row in rows:
1399
+ result.append(dict(zip(columns, row)))
1400
+ return result
1401
+ def visit_ServeStatic(self, node: ServeStatic):
1402
+ folder = str(self.visit(node.folder))
1403
+ url_prefix = str(self.visit(node.url))
1404
+ if not url_prefix.startswith('/'): url_prefix = '/' + url_prefix
1405
+ if not os.path.isdir(folder):
1406
+ print(f"Warning: Static folder '{folder}' does not exist.")
1407
+ self.static_routes[url_prefix] = folder
1408
+ print(f"Serving static files from '{folder}' at '{url_prefix}'")
1409
+ def visit_Every(self, node: Every):
1410
+ interval = self.visit(node.interval)
1411
+ if node.unit == 'minutes': interval *= 60
1412
+ try:
1413
+ while True:
1414
+ for stmt in node.body: self.visit(stmt)
1415
+ time.sleep(interval)
1416
+ except KeyboardInterrupt: pass
1417
+ def visit_After(self, node: After):
1418
+ delay = self.visit(node.delay)
1419
+ if node.unit == 'minutes': delay *= 60
1420
+ time.sleep(delay)
1421
+ for stmt in node.body: self.visit(stmt)
1422
+ def visit_OnRequest(self, node: OnRequest):
1423
+ path_str = self.visit(node.path)
1424
+ if path_str == '__middleware__':
1425
+ self.middleware_routes.append(node.body)
1426
+ return
1427
+ regex_pattern = "^" + path_str + "$"
1428
+ if ':' in path_str:
1429
+ regex_pattern = "^" + re.sub(r':(\w+)', r'(?P<\1>[^/]+)', path_str) + "$"
1430
+ compiled = re.compile(regex_pattern)
1431
+ self.http_routes.append((path_str, compiled, node.body))
1432
+ def visit_Listen(self, node: Listen):
1433
+ port_val = self.visit(node.port)
1434
+ interpreter_ref = self
1435
+ class ShellLiteHandler(BaseHTTPRequestHandler):
1436
+ def log_message(self, format, *args): pass
1437
+ def do_GET(self):
1438
+ self.handle_req()
1439
+ def do_POST(self):
1440
+ content_length = int(self.headers.get('Content-Length', 0))
1441
+ content_type = self.headers.get('Content-Type', '')
1442
+ post_data = self.rfile.read(content_length).decode('utf-8')
1443
+ params = {}
1444
+ json_data = None
1445
+ if 'application/json' in content_type:
1446
+ try:
1447
+ json_data = json.loads(post_data)
1448
+ except:
1449
+ pass
1450
+ else:
1451
+ if post_data:
1452
+ parsed = urllib.parse.parse_qs(post_data)
1453
+ params = {k: v[0] for k, v in parsed.items()}
1454
+ self.handle_req(params, json_data)
1455
+ def do_HEAD(self):
1456
+ self.handle_req()
1457
+ def handle_req(self, post_params=None, json_data=None):
1458
+ try:
1459
+ if post_params is None: post_params = {}
1460
+ path = self.path
1461
+ if '?' in path: path = path.split('?')[0]
1462
+ req_obj = {
1463
+ "method": self.command,
1464
+ "path": path,
1465
+ "params": post_params,
1466
+ "form": post_params,
1467
+ "json": json_data
1468
+ }
1469
+ interpreter_ref.global_env.set("request", req_obj)
1470
+ interpreter_ref.global_env.set("REQUEST_METHOD", self.command)
1471
+ for prefix, folder in interpreter_ref.static_routes.items():
1472
+ if path.startswith(prefix):
1473
+ clean_path = path[len(prefix):]
1474
+ if clean_path.startswith('/'): clean_path = clean_path[1:]
1475
+ if clean_path == '': clean_path = 'index.html'
1476
+ file_path = os.path.join(folder, clean_path)
1477
+ if os.path.exists(file_path) and os.path.isfile(file_path):
1478
+ self.send_response(200)
1479
+ ct = 'application/octet-stream'
1480
+ if file_path.endswith('.css'): ct = 'text/css'
1481
+ elif file_path.endswith('.html'): ct = 'text/html'
1482
+ elif file_path.endswith('.js'): ct = 'application/javascript'
1483
+ self.send_header('Content-Type', ct)
1484
+ self.end_headers()
1485
+ if self.command != 'HEAD':
1486
+ with open(file_path, 'rb') as f: self.wfile.write(f.read())
1487
+ return
1488
+ matched_body = None
1489
+ path_params = {}
1490
+ for pattern, regex, body in interpreter_ref.http_routes:
1491
+ match = regex.match(path)
1492
+ if match:
1493
+ matched_body = body
1494
+ path_params = match.groupdict()
1495
+ break
1496
+ if matched_body:
1497
+ for mw in interpreter_ref.middleware_routes:
1498
+ for stmt in mw: interpreter_ref.visit(stmt)
1499
+ for k, v in path_params.items():
1500
+ interpreter_ref.global_env.set(k, v)
1501
+ for k, v in post_params.items():
1502
+ interpreter_ref.global_env.set(k, v)
1503
+ interpreter_ref.web.stack = []
1504
+ response_body = ""
1505
+ result = None
1506
+ try:
1507
+ for stmt in matched_body:
1508
+ result = interpreter_ref.visit(stmt)
1509
+ except ReturnException as re:
1510
+ result = re.value
1511
+ if interpreter_ref.web.stack:
1512
+ pass
1513
+ if isinstance(result, Tag): response_body = str(result)
1514
+ elif result is not None: response_body = str(result)
1515
+ else: response_body = "OK"
1516
+ self.send_response(200)
1517
+ self.send_header('Content-Type', 'text/html')
1518
+ self.end_headers()
1519
+ if self.command != 'HEAD':
1520
+ self.wfile.write(response_body.encode())
1521
+ else:
1522
+ self.send_response(404)
1523
+ self.end_headers()
1524
+ if self.command != 'HEAD':
1525
+ self.wfile.write(b'Not Found')
1526
+ except Exception as e:
1527
+ print(f"DEBUG: Server Exception: {e}")
1528
+ import traceback
1529
+ traceback.print_exc()
1530
+ try:
1531
+ self.send_response(500)
1532
+ self.end_headers()
1533
+ if self.command != 'HEAD':
1534
+ self.wfile.write(str(e).encode())
1535
+ except: pass
1536
+ server = HTTPServer(('0.0.0.0', port_val), ShellLiteHandler)
1537
+ print(f"\n ShellLite Server v0.04.3 is running!")
1538
+ print(f" \u001b[1;36m➜\u001b[0m Local: \u001b[1;4;36mhttp://localhost:{port_val}/\u001b[0m\n")
1539
+ try: server.serve_forever()
1540
+ except KeyboardInterrupt:
1541
+ print("\n Server stopped.")
1542
+ pass
1543
+ def visit_DatabaseOp(self, node: DatabaseOp):
1544
+ if node.op == 'open':
1545
+ path = self.visit(node.args[0])
1546
+ self.db_conn = sqlite3.connect(path)
1547
+ self.db_conn.row_factory = lambda c, r: {col[0]: r[idx] for idx, col in enumerate(c.description)}
1548
+ return True
1549
+ elif node.op == 'close':
1550
+ if self.db_conn: self.db_conn.close(); self.db_conn = None
1551
+ return True
1552
+ elif node.op == 'exec':
1553
+ if not self.db_conn: raise RuntimeError("Database not open")
1554
+ sql = self.visit(node.args[0])
1555
+ params = []
1556
+ if len(node.args) > 1:
1557
+ val = self.visit(node.args[1])
1558
+ params = val if isinstance(val, list) else [val]
1559
+ c = self.db_conn.cursor(); c.execute(sql, params); self.db_conn.commit()
1560
+ return c.lastrowid
1561
+ elif node.op == 'query':
1562
+ if not self.db_conn: raise RuntimeError("Database not open")
1563
+ sql = self.visit(node.args[0])
1564
+ params = []
1565
+ if len(node.args) > 1:
1566
+ val = self.visit(node.args[1])
1567
+ params = val if isinstance(val, list) else [val]
1568
+ c = self.db_conn.cursor(); c.execute(sql, params)
1569
+ return c.fetchall()
1570
+ def visit_Download(self, node: Download):
1571
+ url = self.visit(node.url)
1572
+ filename = url.split('/')[-1] or "downloaded_file"
1573
+ print(f"Downloading {filename}...")
1574
+ try:
1575
+ with urllib.request.urlopen(url) as response:
1576
+ with open(filename, 'wb') as f:
1577
+ shutil.copyfileobj(response, f)
1578
+ print(f"Download complete: {filename}")
1579
+ except urllib.error.URLError as e:
1580
+ print(f"Error: Could not connect to {url}. Reason: {e}")
1581
+ except PermissionError:
1582
+ print(f"Error: Permission denied writing to {filename}.")
1583
+ except Exception as e:
1584
+ print(f"Error: Download failed: {e}")
1585
+ def visit_ArchiveOp(self, node: ArchiveOp):
1586
+ source = str(self.visit(node.source))
1587
+ target = str(self.visit(node.target))
1588
+ try:
1589
+ if node.op == 'compress':
1590
+ print(f"Compressing '{source}' to '{target}'...")
1591
+ if os.path.isfile(source):
1592
+ with zipfile.ZipFile(target, 'w') as zipf:
1593
+ zipf.write(source, arcname=os.path.basename(source))
1594
+ elif os.path.isdir(source):
1595
+ shutil.make_archive(target.replace('.zip',''), 'zip', source)
1596
+ else:
1597
+ print(f"Error: Source '{source}' does not exist.")
1598
+ return
1599
+ print("Compression complete.")
1600
+ else:
1601
+ print(f"Extracting '{source}' to '{target}'...")
1602
+ if not os.path.exists(source):
1603
+ print(f"Error: Archive '{source}' does not exist.")
1604
+ return
1605
+ with zipfile.ZipFile(source, 'r') as zipf:
1606
+ zipf.extractall(target)
1607
+ print("Extraction complete.")
1608
+ except zipfile.BadZipFile:
1609
+ print(f"Error: '{source}' is not a valid zip file.")
1610
+ except Exception as e:
1611
+ print(f"Error: Archive operation failed: {e}")
1612
+ def visit_CsvOp(self, node: CsvOp):
1613
+ path = self.visit(node.path)
1614
+ if node.op == 'load':
1615
+ with open(path, 'r', newline='') as f:
1616
+ reader = csv.DictReader(f)
1617
+ return [row for row in reader]
1618
+ else:
1619
+ data = self.visit(node.data)
1620
+ if not isinstance(data, list):
1621
+ data = [data]
1622
+ if not data: return
1623
+ rows = []
1624
+ for item in data:
1625
+ if isinstance(item, Instance):
1626
+ rows.append(item.data)
1627
+ elif isinstance(item, dict):
1628
+ rows.append(item)
1629
+ elif isinstance(item, dict):
1630
+ rows.append(item)
1631
+ else:
1632
+ print("Error: Only lists of objects/dictionaries can be saved to CSV.")
1633
+ return
1634
+ if rows:
1635
+ try:
1636
+ keys = rows[0].keys()
1637
+ with open(path, 'w', newline='') as f:
1638
+ writer = csv.DictWriter(f, fieldnames=keys)
1639
+ writer.writeheader()
1640
+ writer.writerows(rows)
1641
+ print(f"Saved {len(rows)} rows to '{path}'.")
1642
+ except Exception as e:
1643
+ print(f"Error saving CSV: {e}")
1644
+ def visit_ClipboardOp(self, node: ClipboardOp):
1645
+ if 'pyperclip' not in sys.modules:
1646
+ raise RuntimeError("Install 'pyperclip' for clipboard support.")
1647
+ if node.op == 'copy':
1648
+ content = str(self.visit(node.content))
1649
+ pyperclip.copy(content)
1650
+ else:
1651
+ return pyperclip.paste()
1652
+ def visit_AutomationOp(self, node: AutomationOp):
1653
+ args = [self.visit(a) for a in node.args]
1654
+ if node.action == 'press':
1655
+ if 'keyboard' not in sys.modules: raise RuntimeError("Install 'keyboard'")
1656
+ keyboard.press_and_release(args[0])
1657
+ elif node.action == 'type':
1658
+ if 'keyboard' not in sys.modules: raise RuntimeError("Install 'keyboard'")
1659
+ keyboard.write(str(args[0]))
1660
+ elif node.action == 'click':
1661
+ if 'mouse' not in sys.modules: raise RuntimeError("Install 'mouse'")
1662
+ mouse.move(args[0], args[1], absolute=True, duration=0.2)
1663
+ mouse.click('left')
1664
+ elif node.action == 'notify':
1665
+ if 'plyer' not in sys.modules: raise RuntimeError("Install 'plyer'")
1666
+ notification.notify(title=str(args[0]), message=str(args[1]))
1667
+ def visit_DateOp(self, node: DateOp):
1668
+ if node.expr == 'today':
1669
+ return datetime.now().strftime("%Y-%m-%d")
1670
+ today = datetime.now()
1671
+ s = node.expr.lower().strip()
1672
+ if s == 'tomorrow':
1673
+ d = today + timedelta(days=1)
1674
+ return d.strftime("%Y-%m-%d")
1675
+ elif s == 'yesterday':
1676
+ d = today - timedelta(days=1)
1677
+ return d.strftime("%Y-%m-%d")
1678
+ days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
1679
+ if s.startswith('next '):
1680
+ day_str = s.replace('next ', '').strip()
1681
+ if day_str in days:
1682
+ target_idx = days.index(day_str)
1683
+ current_idx = today.weekday()
1684
+ days_ahead = target_idx - current_idx
1685
+ if days_ahead <= 0: days_ahead += 7
1686
+ d = today + timedelta(days=days_ahead)
1687
+ return d.strftime("%Y-%m-%d")
1688
+ return s
1689
+ def visit_FileWrite(self, node: FileWrite):
1690
+ path = str(self.visit(node.path))
1691
+ content = str(self.visit(node.content))
1692
+ try:
1693
+ with open(path, node.mode, encoding='utf-8') as f:
1694
+ f.write(content)
1695
+ print(f"{'Appended to' if node.mode == 'a' else 'Written to'} file '{path}'")
1696
+ except Exception as e:
1697
+ raise RuntimeError(f"File operation failed: {e}")
1698
+ def visit_FileRead(self, node: FileRead):
1699
+ path = str(self.visit(node.path))
1700
+ try:
1701
+ with open(path, 'r', encoding='utf-8') as f:
1702
+ return f.read()
1703
+ except FileNotFoundError:
1704
+ raise FileNotFoundError(f"File '{path}' not found.")
1705
+ raise RuntimeError(f"Read failed: {e}")
1706
+ def _builtin_upper(self, s, only_letters=False):
1707
+ if not only_letters:
1708
+ return s.upper()
1709
+ # "UPPER words ONLY LETTERS" -> Uppercase normal letters, leave others?
1710
+ # Or maybe it means "Only extract uppercase letters"?
1711
+ # User output shows: "HELLO WORLD 123" -> "HELLO WORLD 123" (normal)
1712
+ # Wait, user screenshot says:
1713
+ # text = "hello world 123"
1714
+ # words = split text
1715
+ # say upper words only letters
1716
+ # Error: Variable 'only' is not defined.
1717
+ # So maybe they want to UPPERCASE ONLY LETTERS? digits remain same? .upper() does that.
1718
+ # But maybe they mean "remove non-letters"?
1719
+ # "upper words only letters" -> "HELLO WORLD".
1720
+ # If "only letters" means filter?
1721
+ # Let's assume it means "uppercase only the letters" which is standard behavior?
1722
+ # Or maybe "uppercase, and keep only letters".
1723
+ # Let's look at user intent. "upper words only letters".
1724
+ # Likely: Uppercase and remove numbers/symbols?
1725
+ # If input is "hello world 123", output might be "HELLO WORLD".
1726
+ if only_letters:
1727
+ import re
1728
+ return re.sub(r'[^a-zA-Z\s]', '', s).upper()
1729
+ return s.upper()
1730
+
1731
+ def _builtin_sum_range(self, start, end, condition=None):
1732
+ # condition is a string, e.g. "even", "odd", "prime", "digits"
1733
+ total = 0
1734
+ s = int(start)
1735
+ e = int(end)
1736
+ for i in range(s, e + 1):
1737
+ include = True
1738
+ if condition == 'even' and i % 2 != 0: include = False
1739
+ elif condition == 'odd' and i % 2 == 0: include = False
1740
+ elif condition == 'prime':
1741
+ if i < 2: include = False
1742
+ else:
1743
+ for k in range(2, int(i ** 0.5) + 1):
1744
+ if i % k == 0:
1745
+ include = False; break
1746
+ elif condition == 'digits':
1747
+ # sum of digits? Or sum of numbers that are single digits?
1748
+ # "sum of numbers from 1 to 10 when digits" -> unclear.
1749
+ # Assuming "digits" meant specific property.
1750
+ pass
1751
+
1752
+ if include:
1753
+ total += i
1754
+ return total
1755
+
1756
+ def _builtin_range_list(self, start, end, condition=None):
1757
+ res = []
1758
+ s = int(start)
1759
+ e = int(end)
1760
+ for i in range(s, e + 1):
1761
+ include = True
1762
+ if condition == 'even' and i % 2 != 0: include = False
1763
+ elif condition == 'odd' and i % 2 == 0: include = False
1764
+ elif condition == 'prime':
1765
+ if i < 2: include = False
1766
+ else:
1767
+ for k in range(2, int(i ** 0.5) + 1):
1768
+ if i % k == 0:
1769
+ include = False; break
1770
+
1771
+ if include:
1772
+ res.append(i)
1773
+ return res