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/compiler.py
CHANGED
|
@@ -3,46 +3,32 @@ from typing import List
|
|
|
3
3
|
from .ast_nodes import *
|
|
4
4
|
from .runtime import get_std_modules
|
|
5
5
|
import random
|
|
6
|
-
|
|
7
|
-
|
|
8
6
|
class Compiler:
|
|
9
7
|
def __init__(self):
|
|
10
8
|
self.indentation = 0
|
|
11
|
-
|
|
12
9
|
def indent(self):
|
|
13
10
|
return " " * self.indentation
|
|
14
|
-
|
|
15
11
|
def visit(self, node: Node) -> str:
|
|
16
12
|
method_name = f'visit_{type(node).__name__}'
|
|
17
13
|
visitor = getattr(self, method_name, self.generic_visit)
|
|
18
14
|
return visitor(node)
|
|
19
|
-
|
|
20
15
|
def generic_visit(self, node: Node):
|
|
21
16
|
raise Exception(f"Compiler does not support {type(node).__name__}")
|
|
22
|
-
|
|
23
17
|
def compile_block(self, statements: List[Node]) -> str:
|
|
24
18
|
if not statements:
|
|
25
19
|
return f"{self.indent()}pass"
|
|
26
|
-
|
|
27
20
|
code = ""
|
|
28
21
|
code = ""
|
|
29
22
|
for stmt in statements:
|
|
30
23
|
stmt_code = self.visit(stmt)
|
|
31
|
-
|
|
32
|
-
# Auto-handle expressions (Implicit Return + WebBuilder add)
|
|
33
24
|
is_expr = isinstance(stmt, (Number, String, Boolean, Regex, ListVal, Dictionary, SetVal, VarAccess, BinOp, UnaryOp, Call, MethodCall, PropertyAccess, IndexAccess, Await))
|
|
34
25
|
is_block_call = isinstance(stmt, Call) and stmt.body
|
|
35
|
-
|
|
36
26
|
if is_expr and not is_block_call:
|
|
37
27
|
stmt_code = f"_slang_ret = {stmt_code}\n_web_builder.add_text(_slang_ret)"
|
|
38
|
-
|
|
39
|
-
# Indent each line of the statement code
|
|
40
28
|
indented_stmt = "\n".join([f"{self.indent()}{line}" for line in stmt_code.split('\n')])
|
|
41
29
|
code += indented_stmt + "\n"
|
|
42
30
|
return code.rstrip()
|
|
43
|
-
|
|
44
31
|
def compile(self, statements: List[Node]) -> str:
|
|
45
|
-
# Preamble
|
|
46
32
|
code = [
|
|
47
33
|
"import sys",
|
|
48
34
|
"import os",
|
|
@@ -163,88 +149,39 @@ class Compiler:
|
|
|
163
149
|
" globals()[t] = _make_tag_fn(t)",
|
|
164
150
|
"",
|
|
165
151
|
]
|
|
166
|
-
|
|
167
|
-
# Main Code
|
|
168
152
|
code.append("# --- User Script ---")
|
|
169
153
|
code.append(self.compile_block(statements))
|
|
170
|
-
|
|
171
154
|
return "\n".join(code)
|
|
172
|
-
|
|
173
|
-
# --- Visitor Methods ---
|
|
174
|
-
|
|
175
155
|
def visit_Number(self, node: Number):
|
|
176
156
|
return str(node.value)
|
|
177
|
-
|
|
178
157
|
def visit_String(self, node: String):
|
|
179
158
|
return repr(node.value)
|
|
180
|
-
|
|
181
159
|
def visit_Boolean(self, node: Boolean):
|
|
182
160
|
return str(node.value)
|
|
183
|
-
|
|
184
161
|
def visit_Regex(self, node: Regex):
|
|
185
162
|
return f"re.compile({repr(node.pattern)})"
|
|
186
|
-
|
|
187
163
|
def visit_ListVal(self, node: ListVal):
|
|
188
164
|
elements = []
|
|
189
165
|
for e in node.elements:
|
|
190
166
|
if isinstance(e, Spread):
|
|
191
|
-
# Python doesn't support list comprehension spread easily inside list literal in old versions,
|
|
192
|
-
# but [*a, b] works in newer Python. We'll assume [*visit(e.value)]
|
|
193
167
|
elements.append(f"*{self.visit(e.value)}")
|
|
194
168
|
else:
|
|
195
169
|
elements.append(self.visit(e))
|
|
196
170
|
return f"[{', '.join(elements)}]"
|
|
197
|
-
|
|
198
171
|
def visit_Dictionary(self, node: Dictionary):
|
|
199
172
|
pairs = [f"{self.visit(k)}: {self.visit(v)}" for k, v in node.pairs]
|
|
200
173
|
return f"{{{', '.join(pairs)}}}"
|
|
201
|
-
|
|
202
174
|
def visit_SetVal(self, node: SetVal):
|
|
203
175
|
elements = [self.visit(e) for e in node.elements]
|
|
204
176
|
return f"{{{', '.join(elements)}}}"
|
|
205
|
-
|
|
206
177
|
def visit_VarAccess(self, node: VarAccess):
|
|
207
178
|
return node.name
|
|
208
|
-
|
|
209
179
|
def visit_Assign(self, node: Assign):
|
|
210
180
|
return f"{node.name} = {self.visit(node.value)}"
|
|
211
|
-
|
|
212
181
|
def visit_ConstAssign(self, node: ConstAssign):
|
|
213
|
-
# Python doesn't support const, treat as assign
|
|
214
182
|
return f"{node.name} = {self.visit(node.value)}"
|
|
215
|
-
|
|
216
183
|
def visit_PropertyAssign(self, node: PropertyAssign):
|
|
217
|
-
# obj.prop = val OR obj['prop'] = val
|
|
218
|
-
# To support both (like interpreter), we'd need a helper.
|
|
219
|
-
# But let's assume standard object access unless it's a dict.
|
|
220
|
-
# Interpreter check: "if isinstance(instance, Instance): ... elif dict ..."
|
|
221
|
-
# Python handles `obj.prop` and `obj['prop']` differently.
|
|
222
|
-
# Since we mapped `Dictionary` to python `{}` and `Instance` to `Instance` class (which has `data` dict),
|
|
223
|
-
# we need to be careful.
|
|
224
|
-
# IF we want compatibility, `Instance` in runtime should support `__getattr__` hooking to `data`.
|
|
225
|
-
|
|
226
|
-
# Let's assume user uses `Instance` for classes and `dict` for dicts.
|
|
227
|
-
# And `PropertyAssign` in ShellLite means `instance.prop = val`.
|
|
228
|
-
# Code: `instance.data['prop'] = val` if it is an Instance?
|
|
229
|
-
# Or if we implement `__setattr__` on Instance.
|
|
230
|
-
# Let's emit `set_property(obj, prop, val)` helper call?
|
|
231
|
-
# NO, simpler: `node.instance_name`.`property_name` = value?
|
|
232
|
-
|
|
233
|
-
# If I want to match Interpreter perfectly:
|
|
234
|
-
# Interpreter logic: if Instance: inst.data[p]=v. if dict: d[p]=v.
|
|
235
|
-
# I should provide `slang_set_prop(obj, prop, val)` in runtime and use it.
|
|
236
|
-
# But that's slow/ugly code.
|
|
237
|
-
# Let's rely on standard python semantics for now:
|
|
238
|
-
# Logic: If it looks like a dict, treating it as object `x.y = z` fails in Python.
|
|
239
|
-
# So I will emit: `setattr(obj, 'prop', val)` ?? No.
|
|
240
|
-
|
|
241
|
-
# NOTE: `Instance` in `runtime.py` is `class Instance: ... self.data = {}`.
|
|
242
|
-
# It does NOT allow `inst.x = 1`. It requires `inst.data['x'] = 1`.
|
|
243
|
-
# So I MUST use a helper or modify Instance.
|
|
244
|
-
# I will Modify `Instance` in runtime later to allow attribute access.
|
|
245
|
-
# For now, I emit `obj.prop = val` and assume `Instance` supports it.
|
|
246
184
|
return f"{node.instance_name}.{node.property_name} = {self.visit(node.value)}"
|
|
247
|
-
|
|
248
185
|
def visit_BinOp(self, node: BinOp):
|
|
249
186
|
left = self.visit(node.left)
|
|
250
187
|
right = self.visit(node.right)
|
|
@@ -254,75 +191,60 @@ class Compiler:
|
|
|
254
191
|
elif op == 'and' or op == 'or':
|
|
255
192
|
return f"({left} {op} {right})"
|
|
256
193
|
return f"({left} {op} {right})"
|
|
257
|
-
|
|
258
194
|
def visit_UnaryOp(self, node: UnaryOp):
|
|
259
195
|
return f"({node.op} {self.visit(node.right)})"
|
|
260
|
-
|
|
261
196
|
def visit_Print(self, node: Print):
|
|
262
197
|
if node.color or node.style:
|
|
263
198
|
return f"slang_color_print({self.visit(node.expression)}, {repr(node.color)}, {repr(node.style)})"
|
|
264
199
|
return f"print({self.visit(node.expression)})"
|
|
265
|
-
|
|
266
200
|
def visit_Input(self, node: Input):
|
|
267
201
|
if node.prompt:
|
|
268
202
|
return f"input({repr(node.prompt)})"
|
|
269
203
|
return "input()"
|
|
270
|
-
|
|
271
204
|
def visit_If(self, node: If):
|
|
272
205
|
code = f"if {self.visit(node.condition)}:\n"
|
|
273
206
|
self.indentation += 1
|
|
274
207
|
code += self.compile_block(node.body)
|
|
275
208
|
self.indentation -= 1
|
|
276
|
-
|
|
277
209
|
if node.else_body:
|
|
278
210
|
code += f"\n{self.indent()}else:\n"
|
|
279
211
|
self.indentation += 1
|
|
280
212
|
code += self.compile_block(node.else_body)
|
|
281
213
|
self.indentation -= 1
|
|
282
214
|
return code
|
|
283
|
-
|
|
284
215
|
def visit_While(self, node: While):
|
|
285
216
|
code = f"while {self.visit(node.condition)}:\n"
|
|
286
217
|
self.indentation += 1
|
|
287
218
|
code += self.compile_block(node.body)
|
|
288
219
|
self.indentation -= 1
|
|
289
220
|
return code
|
|
290
|
-
|
|
291
221
|
def visit_For(self, node: For):
|
|
292
222
|
code = f"for _ in range({self.visit(node.count)}):\n"
|
|
293
223
|
self.indentation += 1
|
|
294
224
|
code += self.compile_block(node.body)
|
|
295
225
|
self.indentation -= 1
|
|
296
226
|
return code
|
|
297
|
-
|
|
298
227
|
def visit_ForIn(self, node: ForIn):
|
|
299
228
|
code = f"for {node.var_name} in {self.visit(node.iterable)}:\n"
|
|
300
229
|
self.indentation += 1
|
|
301
230
|
code += self.compile_block(node.body)
|
|
302
231
|
self.indentation -= 1
|
|
303
232
|
return code
|
|
304
|
-
|
|
305
233
|
def visit_Repeat(self, node: Repeat):
|
|
306
|
-
return self.visit_For(For(node.count, node.body))
|
|
307
|
-
|
|
234
|
+
return self.visit_For(For(node.count, node.body))
|
|
308
235
|
def visit_Forever(self, node: Forever):
|
|
309
236
|
code = f"while True:\n"
|
|
310
237
|
self.indentation += 1
|
|
311
238
|
code += self.compile_block(node.body)
|
|
312
239
|
self.indentation -= 1
|
|
313
240
|
return code
|
|
314
|
-
|
|
315
241
|
def visit_Until(self, node: Until):
|
|
316
|
-
# until X -> while not X
|
|
317
242
|
code = f"while not ({self.visit(node.condition)}):\n"
|
|
318
243
|
self.indentation += 1
|
|
319
244
|
code += self.compile_block(node.body)
|
|
320
245
|
self.indentation -= 1
|
|
321
246
|
return code
|
|
322
|
-
|
|
323
247
|
def visit_ProgressLoop(self, node: ProgressLoop):
|
|
324
|
-
# Desugar progress loop to a python loop with progress bar
|
|
325
|
-
# We'll use a wrapper if it's a range loop or iterable
|
|
326
248
|
loop = node.loop_node
|
|
327
249
|
if isinstance(loop, (For, Repeat)):
|
|
328
250
|
count_expr = self.visit(loop.count)
|
|
@@ -346,30 +268,24 @@ class Compiler:
|
|
|
346
268
|
code += f"\n{self.indent()}print(f'Progress: [{{(\"=\"*20)}}] 100%')"
|
|
347
269
|
return code
|
|
348
270
|
return "# Progress only supported for range/in loops"
|
|
349
|
-
|
|
350
271
|
def visit_Convert(self, node: Convert):
|
|
351
272
|
if node.target_format.lower() == 'json':
|
|
352
273
|
return f"slang_json_stringify({self.visit(node.expression)})"
|
|
353
274
|
return f"{self.visit(node.expression)} # Unknown format"
|
|
354
|
-
|
|
355
275
|
def visit_Download(self, node: Download):
|
|
356
276
|
return f"slang_download({self.visit(node.url)})"
|
|
357
|
-
|
|
358
277
|
def visit_ArchiveOp(self, node: ArchiveOp):
|
|
359
278
|
return f"slang_archive({repr(node.op)}, {self.visit(node.source)}, {self.visit(node.target)})"
|
|
360
|
-
|
|
361
279
|
def visit_CsvOp(self, node: CsvOp):
|
|
362
280
|
if node.op == 'load':
|
|
363
281
|
return f"slang_csv_load({self.visit(node.path)})"
|
|
364
282
|
else:
|
|
365
283
|
return f"slang_csv_save({self.visit(node.data)}, {self.visit(node.path)})"
|
|
366
|
-
|
|
367
284
|
def visit_ClipboardOp(self, node: ClipboardOp):
|
|
368
285
|
if node.op == 'copy':
|
|
369
286
|
return f"slang_clipboard_copy({self.visit(node.content)})"
|
|
370
287
|
else:
|
|
371
288
|
return f"slang_clipboard_paste()"
|
|
372
|
-
|
|
373
289
|
def visit_AutomationOp(self, node: AutomationOp):
|
|
374
290
|
args = [self.visit(a) for a in node.args]
|
|
375
291
|
if node.action == 'press': return f"slang_press({args[0]})"
|
|
@@ -377,16 +293,12 @@ class Compiler:
|
|
|
377
293
|
if node.action == 'click': return f"slang_click({args[0]}, {args[1]})"
|
|
378
294
|
if node.action == 'notify': return f"slang_notify({args[0]}, {args[1]})"
|
|
379
295
|
return "pass"
|
|
380
|
-
|
|
381
296
|
def visit_DateOp(self, node: DateOp):
|
|
382
297
|
return f"slang_date_parse({repr(node.expr)})"
|
|
383
|
-
|
|
384
298
|
def visit_FileWrite(self, node: FileWrite):
|
|
385
299
|
return f"slang_file_write({self.visit(node.path)}, {self.visit(node.content)}, {repr(node.mode)})"
|
|
386
|
-
|
|
387
300
|
def visit_FileRead(self, node: FileRead):
|
|
388
301
|
return f"slang_file_read({self.visit(node.path)})"
|
|
389
|
-
|
|
390
302
|
def visit_DatabaseOp(self, node: DatabaseOp):
|
|
391
303
|
if node.op == 'open': return f"slang_db_open({self.visit(node.args[0])})"
|
|
392
304
|
if node.op == 'close': return f"slang_db_close()"
|
|
@@ -397,7 +309,6 @@ class Compiler:
|
|
|
397
309
|
params = self.visit(node.args[1]) if len(node.args) > 1 else "[]"
|
|
398
310
|
return f"slang_db_query({self.visit(node.args[0])}, {params})"
|
|
399
311
|
return "None"
|
|
400
|
-
|
|
401
312
|
def visit_Every(self, node: Every):
|
|
402
313
|
interval = self.visit(node.interval)
|
|
403
314
|
if node.unit == 'minutes': interval = f"({interval} * 60)"
|
|
@@ -407,14 +318,12 @@ class Compiler:
|
|
|
407
318
|
code += f"\n{self.indent()}time.sleep({interval})"
|
|
408
319
|
self.indentation -= 1
|
|
409
320
|
return code
|
|
410
|
-
|
|
411
321
|
def visit_After(self, node: After):
|
|
412
322
|
delay = self.visit(node.delay)
|
|
413
323
|
if node.unit == 'minutes': delay = f"({delay} * 60)"
|
|
414
324
|
code = f"time.sleep({delay})\n"
|
|
415
325
|
code += self.compile_block(node.body)
|
|
416
326
|
return code
|
|
417
|
-
|
|
418
327
|
def visit_FunctionDef(self, node: FunctionDef):
|
|
419
328
|
args_strs = []
|
|
420
329
|
for arg_name, default_node, type_hint in node.args:
|
|
@@ -422,253 +331,158 @@ class Compiler:
|
|
|
422
331
|
args_strs.append(f"{arg_name}={self.visit(default_node)}")
|
|
423
332
|
else:
|
|
424
333
|
args_strs.append(arg_name)
|
|
425
|
-
|
|
426
334
|
code = f"def {node.name}({', '.join(args_strs)}):\n"
|
|
427
|
-
|
|
428
|
-
# Save and reset indentation for relative block generation
|
|
429
335
|
old_indent = self.indentation
|
|
430
336
|
self.indentation = 1
|
|
431
|
-
|
|
432
337
|
code += f"{self.indent()}_slang_ret = None\n"
|
|
433
338
|
code += self.compile_block(node.body)
|
|
434
339
|
code += f"\n{self.indent()}return _slang_ret"
|
|
435
|
-
|
|
436
340
|
self.indentation = old_indent
|
|
437
341
|
return code
|
|
438
|
-
|
|
439
342
|
def visit_Return(self, node: Return):
|
|
440
343
|
return f"return {self.visit(node.value)}"
|
|
441
|
-
|
|
442
344
|
def visit_Call(self, node: Call):
|
|
443
345
|
args = [self.visit(a) for a in node.args]
|
|
444
346
|
call_expr = f"{node.name}({', '.join(args)})"
|
|
445
|
-
|
|
446
347
|
if node.body:
|
|
447
|
-
# Block context for DSL
|
|
448
348
|
var_name = f"_tag_{random.randint(0, 1000000)}"
|
|
449
349
|
code = f"{var_name} = {call_expr}\n"
|
|
450
350
|
code += f"with BuilderContext({var_name}):\n"
|
|
451
|
-
|
|
452
351
|
old_indent = self.indentation
|
|
453
352
|
self.indentation = 1
|
|
454
353
|
code += self.compile_block(node.body)
|
|
455
354
|
self.indentation = old_indent
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
# Capture result
|
|
459
355
|
code += f"\n_slang_ret = {var_name}"
|
|
460
356
|
code += f"\n_web_builder.add_text({var_name})"
|
|
461
357
|
return code
|
|
462
|
-
|
|
463
358
|
return call_expr
|
|
464
|
-
|
|
465
359
|
def visit_ClassDef(self, node: ClassDef):
|
|
466
|
-
# class Name(Parent or Instance):
|
|
467
360
|
parent = node.parent if node.parent else "Instance"
|
|
468
361
|
code = f"class {node.name}({parent}):\n"
|
|
469
362
|
self.indentation += 1
|
|
470
|
-
|
|
471
|
-
# Init method to setup properties
|
|
472
363
|
args = ["self"] + node.properties
|
|
473
364
|
assigns = [f"self.{p} = {p}" for p in node.properties]
|
|
474
365
|
if not assigns:
|
|
475
366
|
assigns = ["pass"]
|
|
476
|
-
|
|
477
367
|
code += f"{self.indent()}def __init__({', '.join(args)}):\n"
|
|
478
|
-
# Call super? If inheriting, yes.
|
|
479
|
-
# But we don't know if parent has init args easily without context.
|
|
480
|
-
# Assumption: simple data classes.
|
|
481
|
-
|
|
482
|
-
# Revisit Instance: Instance in runtime expects class_def.
|
|
483
|
-
# But here we are compiling to Python Classes.
|
|
484
|
-
# So we don't need `Instance` wrapper! We make real classes!
|
|
485
|
-
# `class Robot:` .... `r = Robot()`.
|
|
486
|
-
# This is much better.
|
|
487
|
-
|
|
488
368
|
self.indentation += 1
|
|
489
369
|
for assign in assigns:
|
|
490
370
|
code += f"{self.indent()}{assign}\n"
|
|
491
371
|
self.indentation -= 1
|
|
492
|
-
|
|
493
|
-
# Methods
|
|
494
372
|
for method in node.methods:
|
|
495
|
-
# Need to add 'self' to args
|
|
496
373
|
old_args = method.args
|
|
497
|
-
# We construct a new node or just simulate visiting
|
|
498
374
|
m_args = ["self"]
|
|
499
375
|
for arg_name, default_node, type_hint in method.args:
|
|
500
376
|
if default_node:
|
|
501
377
|
m_args.append(f"{arg_name}={self.visit(default_node)}")
|
|
502
378
|
else:
|
|
503
379
|
m_args.append(arg_name)
|
|
504
|
-
|
|
505
380
|
code += f"\n{self.indent()}def {method.name}({', '.join(m_args)}):\n"
|
|
506
381
|
self.indentation += 1
|
|
507
382
|
code += self.compile_block(method.body)
|
|
508
383
|
self.indentation -= 1
|
|
509
|
-
|
|
510
384
|
self.indentation -= 1
|
|
511
385
|
return code
|
|
512
|
-
|
|
513
386
|
def visit_Instantiation(self, node: Instantiation):
|
|
514
387
|
args = [self.visit(a) for a in node.args]
|
|
515
|
-
# var = Class(args)
|
|
516
388
|
return f"{node.var_name} = {node.class_name}({', '.join(args)})"
|
|
517
|
-
|
|
518
389
|
def visit_Make(self, node: Make):
|
|
519
390
|
args = [self.visit(a) for a in node.args]
|
|
520
391
|
return f"{node.class_name}({', '.join(args)})"
|
|
521
|
-
|
|
522
392
|
def visit_MethodCall(self, node: MethodCall):
|
|
523
393
|
args = [self.visit(a) for a in node.args]
|
|
524
394
|
return f"{node.instance_name}.{node.method_name}({', '.join(args)})"
|
|
525
|
-
|
|
526
395
|
def visit_PropertyAccess(self, node: PropertyAccess):
|
|
527
396
|
return f"{node.instance_name}.{node.property_name}"
|
|
528
|
-
|
|
529
397
|
def visit_Import(self, node: Import):
|
|
530
|
-
# module imports
|
|
531
398
|
if node.path in ('math', 'time', 'http', 'env', 'args', 'path', 're'):
|
|
532
|
-
# We rely on STD_MODULES wrapper being present via runtime preamble
|
|
533
399
|
return f"{node.path} = STD_MODULES['{node.path}']"
|
|
534
400
|
else:
|
|
535
|
-
# File import?
|
|
536
|
-
# `import foo` from foo.py?
|
|
537
|
-
# Or compile that file too and import?
|
|
538
|
-
# For now: `exec(slang_read('{node.path}'))` ?? NO, that's interpreting.
|
|
539
|
-
# Python Imports: `import x`
|
|
540
401
|
base = os.path.basename(node.path).replace('.shl', '').replace('.py', '')
|
|
541
402
|
return f"import {base}"
|
|
542
|
-
|
|
543
403
|
def visit_ImportAs(self, node: ImportAs):
|
|
544
404
|
if node.path in ('math', 'time', 'http', 'env', 'args', 'path', 're'):
|
|
545
405
|
return f"{node.alias} = STD_MODULES['{node.path}']"
|
|
546
406
|
base = os.path.basename(node.path).replace('.shl', '').replace('.py', '')
|
|
547
407
|
return f"import {base} as {node.alias}"
|
|
548
|
-
|
|
549
408
|
def visit_Try(self, node: Try):
|
|
550
409
|
code = f"try:\n"
|
|
551
410
|
self.indentation += 1
|
|
552
411
|
code += self.compile_block(node.try_body)
|
|
553
412
|
self.indentation -= 1
|
|
554
|
-
|
|
555
413
|
code += f"\n{self.indent()}except Exception as {node.catch_var}:\n"
|
|
556
414
|
self.indentation += 1
|
|
557
415
|
code += self.compile_block(node.catch_body)
|
|
558
416
|
self.indentation -= 1
|
|
559
417
|
return code
|
|
560
|
-
|
|
561
418
|
def visit_TryAlways(self, node: TryAlways):
|
|
562
419
|
code = f"try:\n"
|
|
563
420
|
self.indentation += 1
|
|
564
421
|
code += self.compile_block(node.try_body)
|
|
565
422
|
self.indentation -= 1
|
|
566
|
-
|
|
567
423
|
if node.catch_body:
|
|
568
424
|
code += f"\n{self.indent()}except Exception as {node.catch_var}:\n"
|
|
569
425
|
self.indentation += 1
|
|
570
426
|
code += self.compile_block(node.catch_body)
|
|
571
427
|
self.indentation -= 1
|
|
572
|
-
|
|
573
428
|
code += f"\n{self.indent()}finally:\n"
|
|
574
429
|
self.indentation += 1
|
|
575
430
|
code += self.compile_block(node.always_body)
|
|
576
431
|
self.indentation -= 1
|
|
577
432
|
return code
|
|
578
|
-
|
|
579
433
|
def visit_Throw(self, node: Throw):
|
|
580
434
|
return f"raise Exception({self.visit(node.message)})"
|
|
581
|
-
|
|
582
435
|
def visit_Stop(self, node: Stop): return "break"
|
|
583
436
|
def visit_Skip(self, node: Skip): return "continue"
|
|
584
437
|
def visit_Exit(self, node: Exit):
|
|
585
438
|
code = self.visit(node.code) if node.code else "0"
|
|
586
439
|
return f"sys.exit({code})"
|
|
587
|
-
|
|
588
440
|
def visit_ListComprehension(self, node: ListComprehension):
|
|
589
|
-
# [expr for var in iterable if cond]
|
|
590
441
|
iter_str = self.visit(node.iterable)
|
|
591
442
|
expr_str = self.visit(node.expr)
|
|
592
443
|
cond_str = f" if {self.visit(node.condition)}" if node.condition else ""
|
|
593
444
|
return f"[{expr_str} for {node.var_name} in {iter_str}{cond_str}]"
|
|
594
|
-
|
|
595
445
|
def visit_Lambda(self, node: Lambda):
|
|
596
|
-
# lambda p1, p2: expr
|
|
597
446
|
return f"lambda {', '.join(node.params)}: {self.visit(node.body)}"
|
|
598
|
-
|
|
599
447
|
def visit_Ternary(self, node: Ternary):
|
|
600
448
|
return f"({self.visit(node.true_expr)} if {self.visit(node.condition)} else {self.visit(node.false_expr)})"
|
|
601
|
-
|
|
602
|
-
# --- System / Threads ---
|
|
603
|
-
|
|
604
449
|
def visit_Spawn(self, node: Spawn):
|
|
605
|
-
# _executor.submit(func, *args)
|
|
606
|
-
# Call node is call(name, args).
|
|
607
|
-
# We need to construct: submit(name, *args)
|
|
608
450
|
if isinstance(node.call, Call):
|
|
609
451
|
args = [self.visit(a) for a in node.call.args]
|
|
610
452
|
return f"_executor.submit({node.call.name}, {', '.join(args)})"
|
|
611
|
-
return f"_executor.submit({self.visit(node.call)})"
|
|
612
|
-
|
|
453
|
+
return f"_executor.submit({self.visit(node.call)})"
|
|
613
454
|
def visit_Await(self, node: Await):
|
|
614
455
|
return f"{self.visit(node.task)}.result()"
|
|
615
|
-
|
|
616
|
-
# --- Listener / HTTP --- (Simplified for compilation)
|
|
617
|
-
|
|
618
456
|
def visit_Listen(self, node: Listen):
|
|
619
|
-
# We need to inject the Handler class HERE or rely on a generic one?
|
|
620
|
-
# The Handler needs access to `http_routes` which might be defined dynamically.
|
|
621
|
-
# We'll use a global routes dict in the generated code.
|
|
622
457
|
port = self.visit(node.port)
|
|
623
458
|
code = f"server_address = ('', {port})\n"
|
|
624
459
|
code += f"{self.indent()}httpd = HTTPServer(server_address, ShellLiteHTTPHandler)\n"
|
|
625
460
|
code += f"{self.indent()}print(f'Serving on port {{server_address[1]}}...')\n"
|
|
626
461
|
code += f"{self.indent()}httpd.serve_forever()"
|
|
627
462
|
return code
|
|
628
|
-
|
|
629
463
|
def visit_ServeStatic(self, node: ServeStatic):
|
|
630
464
|
return f"GLOBAL_STATIC_ROUTES[{self.visit(node.url)}] = {self.visit(node.folder)}"
|
|
631
|
-
|
|
632
465
|
def visit_OnRequest(self, node: OnRequest):
|
|
633
|
-
# registers route to GLOBAL_ROUTES
|
|
634
|
-
# We need to define ShellLiteHTTPHandler that uses GLOBAL_ROUTES
|
|
635
|
-
# We should add this to Preamble!
|
|
636
466
|
path = self.visit(node.path)
|
|
637
|
-
|
|
638
|
-
# We need to wrap the body in a function?
|
|
639
|
-
# on request "/foo": ... body ...
|
|
640
|
-
# -> def handler_foo(): ... body ...
|
|
641
|
-
# -> GLOBAL_ROUTES["/foo"] = handler_foo
|
|
642
|
-
|
|
643
467
|
func_name = f"route_handler_{abs(hash(str(node.path)))}_{random.randint(0,1000)}"
|
|
644
|
-
|
|
645
468
|
code = f"def {func_name}():\n"
|
|
646
|
-
|
|
647
469
|
old_indent = self.indentation
|
|
648
470
|
self.indentation = 1
|
|
649
|
-
|
|
650
471
|
code += f"{self.indent()}_slang_ret = None\n"
|
|
651
472
|
code += self.compile_block(node.body)
|
|
652
473
|
code += f"\n{self.indent()}return _slang_ret"
|
|
653
|
-
|
|
654
474
|
self.indentation = old_indent
|
|
655
|
-
|
|
656
475
|
code += f"\nGLOBAL_ROUTES[{path}] = {func_name}"
|
|
657
476
|
return code
|
|
658
|
-
|
|
659
477
|
def visit_Alert(self, node: Alert):
|
|
660
478
|
return f"slang_alert({self.visit(node.message)})"
|
|
661
|
-
|
|
662
479
|
def visit_Prompt(self, node: Prompt):
|
|
663
480
|
return f"slang_prompt({self.visit(node.prompt)})"
|
|
664
|
-
|
|
665
481
|
def visit_Confirm(self, node: Confirm):
|
|
666
482
|
return f"slang_confirm({self.visit(node.prompt)})"
|
|
667
|
-
|
|
668
483
|
def visit_FileWatcher(self, node: FileWatcher):
|
|
669
484
|
path_var = f"fw_path_{random.randint(0,1000)}"
|
|
670
485
|
mtime_var = f"fw_mtime_{random.randint(0,1000)}"
|
|
671
|
-
|
|
672
486
|
code = f"{path_var} = {self.visit(node.path)}\n"
|
|
673
487
|
code += f"{self.indent()}{mtime_var} = os.path.getmtime({path_var}) if os.path.exists({path_var}) else 0\n"
|
|
674
488
|
code += f"{self.indent()}while True:\n"
|
|
@@ -685,4 +499,3 @@ class Compiler:
|
|
|
685
499
|
self.indentation -= 1
|
|
686
500
|
self.indentation -= 1
|
|
687
501
|
return code
|
|
688
|
-
|
shell_lite/formatter.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from .lexer import Lexer, Token
|
|
3
|
+
class Formatter:
|
|
4
|
+
def __init__(self, source_code: str):
|
|
5
|
+
self.source_code = source_code
|
|
6
|
+
self.indent_size = 4
|
|
7
|
+
def format(self) -> str:
|
|
8
|
+
lexer = Lexer(self.source_code)
|
|
9
|
+
try:
|
|
10
|
+
tokens = lexer.tokenize()
|
|
11
|
+
except Exception:
|
|
12
|
+
raise
|
|
13
|
+
formatted_lines = []
|
|
14
|
+
current_indent = 0
|
|
15
|
+
current_line_tokens: List[Token] = []
|
|
16
|
+
def flush_line():
|
|
17
|
+
nonlocal current_line_tokens
|
|
18
|
+
if not current_line_tokens:
|
|
19
|
+
pass
|
|
20
|
+
line_str = self._format_line_tokens(current_line_tokens, current_indent)
|
|
21
|
+
formatted_lines.append(line_str)
|
|
22
|
+
current_line_tokens.clear()
|
|
23
|
+
for token in tokens:
|
|
24
|
+
if token.type == 'EOF':
|
|
25
|
+
if current_line_tokens:
|
|
26
|
+
flush_line()
|
|
27
|
+
break
|
|
28
|
+
elif token.type == 'INDENT':
|
|
29
|
+
current_indent += 1
|
|
30
|
+
elif token.type == 'DEDENT':
|
|
31
|
+
current_indent -= 1
|
|
32
|
+
if current_indent < 0: current_indent = 0
|
|
33
|
+
elif token.type == 'NEWLINE':
|
|
34
|
+
flush_line()
|
|
35
|
+
pass
|
|
36
|
+
else:
|
|
37
|
+
current_line_tokens.append(token)
|
|
38
|
+
return '\n'.join(formatted_lines)
|
|
39
|
+
def _format_line_tokens(self, tokens: List[Token], indent_level: int) -> str:
|
|
40
|
+
if not tokens:
|
|
41
|
+
return ''
|
|
42
|
+
line_parts = []
|
|
43
|
+
line_parts.append(' ' * (indent_level * self.indent_size))
|
|
44
|
+
for i, token in enumerate(tokens):
|
|
45
|
+
val = token.value
|
|
46
|
+
type = token.type
|
|
47
|
+
if type == 'STRING':
|
|
48
|
+
if '"' in val and "'" not in val:
|
|
49
|
+
val = f"'{val}'"
|
|
50
|
+
else:
|
|
51
|
+
val = val.replace('"', '\\"')
|
|
52
|
+
val = f'"{val}"'
|
|
53
|
+
elif type == 'REGEX':
|
|
54
|
+
val = f"/{val}/"
|
|
55
|
+
if i > 0:
|
|
56
|
+
prev = tokens[i-1]
|
|
57
|
+
need_space = True
|
|
58
|
+
if prev.type in ('LPAREN', 'LBRACKET', 'LBRACE', 'DOT', 'AT'):
|
|
59
|
+
need_space = False
|
|
60
|
+
if type in ('RPAREN', 'RBRACKET', 'RBRACE', 'DOT', 'COMMA', 'COLON'):
|
|
61
|
+
need_space = False
|
|
62
|
+
if type == 'LPAREN':
|
|
63
|
+
if prev.type == 'ID':
|
|
64
|
+
need_space = False
|
|
65
|
+
elif prev.type in ('RPAREN', 'RBRACKET', 'STRING'):
|
|
66
|
+
need_space = False
|
|
67
|
+
else:
|
|
68
|
+
pass
|
|
69
|
+
if type == 'LBRACKET':
|
|
70
|
+
if prev.type in ('ID', 'STRING', 'RPAREN', 'RBRACKET'):
|
|
71
|
+
need_space = False
|
|
72
|
+
if need_space:
|
|
73
|
+
line_parts.append(' ')
|
|
74
|
+
line_parts.append(val)
|
|
75
|
+
return "".join(line_parts).rstrip()
|