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/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)) # Same as For count
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)})" # Fallback
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
-
@@ -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()