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