shell-lite 0.4.3__py3-none-any.whl → 0.4.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shell_lite/interpreter.py +23 -23
- shell_lite/lexer.py +16 -8
- shell_lite/main.py +5 -5
- shell_lite/parser.py +263 -113
- {shell_lite-0.4.3.dist-info → shell_lite-0.4.4.dist-info}/METADATA +1 -1
- shell_lite-0.4.4.dist-info/RECORD +15 -0
- shell_lite/fix_nulls.py +0 -29
- shell_lite/formatter.py +0 -75
- shell_lite/interpreter_backup.py +0 -1781
- shell_lite/interpreter_final.py +0 -1773
- shell_lite/interpreter_new.py +0 -1773
- shell_lite/js_compiler.py +0 -220
- shell_lite/lexer_new.py +0 -252
- shell_lite/minimal_interpreter.py +0 -25
- shell_lite/parser_new.py +0 -2229
- shell_lite/patch_parser.py +0 -41
- shell_lite-0.4.3.dist-info/RECORD +0 -25
- {shell_lite-0.4.3.dist-info → shell_lite-0.4.4.dist-info}/LICENSE +0 -0
- {shell_lite-0.4.3.dist-info → shell_lite-0.4.4.dist-info}/WHEEL +0 -0
- {shell_lite-0.4.3.dist-info → shell_lite-0.4.4.dist-info}/entry_points.txt +0 -0
- {shell_lite-0.4.3.dist-info → shell_lite-0.4.4.dist-info}/top_level.txt +0 -0
shell_lite/interpreter_final.py
DELETED
|
@@ -1,1773 +0,0 @@
|
|
|
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: VERSION 2 LOADED')
|
|
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
|