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