mlog-py 0.8.1__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.
mlog/__init__.py ADDED
@@ -0,0 +1,664 @@
1
+ """Main API and CLI interface for Python-to-mlog compiler."""
2
+
3
+ import argparse
4
+ import builtins
5
+ import os
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from enum import IntEnum
9
+ from typing import Callable, Dict, List, Optional
10
+
11
+ _cli_print = builtins.print
12
+
13
+ from .compiler import Compiler
14
+ from .emitter import Emitter
15
+ from .errors import CompileError
16
+ from .ir import IRNode
17
+ from .mlog_registry import (
18
+ Align,
19
+ Condition,
20
+ ControlProperty,
21
+ DrawType,
22
+ Items,
23
+ Liquids,
24
+ LogicOp,
25
+ LookupType,
26
+ RadarSort,
27
+ RadarTarget,
28
+ SensorProperty,
29
+ Teams,
30
+ UnitControl,
31
+ Units,
32
+ TileLayer,
33
+ QueryShape,
34
+ QueryType,
35
+ StatusEffects,
36
+ FetchType,
37
+ LogicRules,
38
+ Weathers,
39
+ Fx,
40
+ MessageType,
41
+ CutsceneAction,
42
+ MarkerType,
43
+ MarkerControl,
44
+ Vars,
45
+ BuiltinVarToken,
46
+ )
47
+ from .metadata import (
48
+ Array,
49
+ ColorChannels,
50
+ LocateResult,
51
+ MLOG_EXPORTS,
52
+ OreLocateResult,
53
+ _ControlCallable,
54
+ _CutsceneCallable,
55
+ _DrawCallable,
56
+ _OpCallable,
57
+ _UnitControlCallable,
58
+ _UnitLocateCallable,
59
+ _SoundCallable,
60
+ _MusicCallable,
61
+ _MarkerCallable,
62
+ sound,
63
+ music,
64
+ marker,
65
+ _rt_bullet as bullet,
66
+ _rt_clientdata as clientdata,
67
+ _rt_control as control,
68
+ _rt_cutscene as cutscene,
69
+ _rt_draw as draw,
70
+ _rt_drawflush as drawflush,
71
+ _rt_effect as effect,
72
+ _rt_end as end,
73
+ _rt_explosion as explosion,
74
+ _rt_fetch as fetch,
75
+ _rt_format as format,
76
+ _rt_getblock as getblock,
77
+ _rt_getflag as getflag,
78
+ _rt_getlink as getlink,
79
+ _rt_jump as jump,
80
+ _rt_localeprint as localeprint,
81
+ _rt_lookup as lookup,
82
+ _rt_makemarker as makemarker,
83
+ _rt_max as max,
84
+ _rt_message as message,
85
+ _rt_min as min,
86
+ _rt_noop as noop,
87
+ _rt_op as op,
88
+ _rt_packcolor as packcolor,
89
+ _rt_playmusic as playmusic,
90
+ _rt_playsound as playsound,
91
+ _rt_print as print,
92
+ _rt_printchar as printchar,
93
+ _rt_printflush as printflush,
94
+ _rt_query as query,
95
+ _rt_radar as radar,
96
+ _rt_raw as raw,
97
+ _rt_read as read,
98
+ _rt_select as select,
99
+ _rt_sensor as sensor,
100
+ _rt_set as set,
101
+ _rt_setblock as setblock,
102
+ _rt_setflag as setflag,
103
+ _rt_setmarker as setmarker,
104
+ _rt_setprop as setprop,
105
+ _rt_setrate as setrate,
106
+ _rt_setrule as setrule,
107
+ _rt_spawn as spawn,
108
+ _rt_spawnwave as spawnwave,
109
+ _rt_status as status,
110
+ _rt_stop as stop,
111
+ _rt_sync as sync,
112
+ _rt_ubind as ubind,
113
+ _rt_ucontrol as ucontrol,
114
+ _rt_ulocate as ulocate,
115
+ _rt_unpackcolor as unpackcolor,
116
+ _rt_uradar as uradar,
117
+ _rt_wait as wait,
118
+ _rt_weathersense as weathersense,
119
+ _rt_weatherset as weatherset,
120
+ _rt_write as write,
121
+ )
122
+ from .optimizer import PassThroughOptimizer
123
+ from .parser import parse_and_validate
124
+ from .validator import MlogValidator, ValidationError
125
+ from .comments import transform_comments_to_print
126
+ from .decompiler import decompile, decompile_with_source_map
127
+
128
+ # Null constant representing null in Mindustry Logic
129
+
130
+ null = None
131
+
132
+
133
+ def _get_version() -> str:
134
+ """Resolve package version from pyproject.toml or packaging metadata."""
135
+ try:
136
+ pyproject_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "pyproject.toml"))
137
+ if os.path.exists(pyproject_path):
138
+ with open(pyproject_path, "r", encoding="utf-8") as f:
139
+ for line in f:
140
+ line = line.strip()
141
+ if line.startswith("version ="):
142
+ return line.split("=", 1)[1].strip().strip('"').strip("'")
143
+ except Exception:
144
+ pass
145
+
146
+ try:
147
+ from importlib.metadata import version, PackageNotFoundError
148
+ try:
149
+ return version("mlog-py")
150
+ except PackageNotFoundError:
151
+ pass
152
+ except ImportError:
153
+ pass
154
+
155
+ return "0.8.1"
156
+
157
+
158
+ __version__: str = _get_version()
159
+
160
+ __all__ = sorted(list(MLOG_EXPORTS))
161
+
162
+
163
+
164
+ @dataclass
165
+ class CompileResult:
166
+ """Result of compilation containing final mlog text, IR, and label table."""
167
+ mlog: str
168
+ ir: List[IRNode]
169
+ label_table: Dict[str, int]
170
+
171
+
172
+ def compile_py(
173
+ source: str,
174
+ filename: str = "<stdin>",
175
+ optimize: bool = False,
176
+ validate: bool = True,
177
+ allow_functions: bool = False,
178
+ fold_constants: bool = True,
179
+ comments_to_print: bool = False,
180
+ ) -> CompileResult:
181
+ """Compile Python subset source code into canonical vanilla mlog.
182
+
183
+ Args:
184
+ source: Python code string.
185
+ filename: Source file name for error reporting.
186
+ optimize: Enable optimization passes (PassThrough in Phase 1).
187
+ validate: Validate emitted mlog against Mindustry syntax rules.
188
+ allow_functions: Allow user-defined function definitions and calls.
189
+ fold_constants: Enable compile-time constant folding (default: True).
190
+ comments_to_print: Convert Python comments into Mindustry 'print' instructions.
191
+
192
+ Returns:
193
+ CompileResult containing the emitted mlog, IR instructions, and label table.
194
+ """
195
+ # 0. Optional comments-to-print preprocessing
196
+ processed_source, is_comments_to_print = transform_comments_to_print(
197
+ source, comments_to_print=comments_to_print
198
+ )
199
+
200
+ # 1. Parse & validate Python AST subset
201
+ tree = parse_and_validate(processed_source, filename, allow_functions=allow_functions)
202
+
203
+ # 2. Lower AST to IR
204
+ compiler = Compiler(
205
+ filename,
206
+ allow_functions=allow_functions,
207
+ fold_constants=fold_constants,
208
+ comments_to_print=is_comments_to_print,
209
+ )
210
+ ir = compiler.compile(tree)
211
+
212
+ # 3. Optimize (Phase 1: PassThrough)
213
+ optimizer = PassThroughOptimizer()
214
+ ir = optimizer.optimize(ir)
215
+
216
+ # 4. Two-pass emit & label resolution
217
+ emitter = Emitter(ir)
218
+ mlog_text, label_table = emitter.emit()
219
+
220
+ # 5. Validate emitted mlog text
221
+ if validate and mlog_text.strip():
222
+ MlogValidator.validate(mlog_text)
223
+
224
+ return CompileResult(mlog=mlog_text, ir=ir, label_table=label_table)
225
+
226
+
227
+ def _cli_compile(args: argparse.Namespace) -> int:
228
+ """Handle compile subcommand: Python DSL -> MLog."""
229
+ input_source = args.input_file
230
+ is_stdin = (input_source is None or input_source == "-")
231
+
232
+ if is_stdin:
233
+ filename = "<stdin>"
234
+ is_tty = hasattr(sys.stdin, "isatty") and sys.stdin.isatty()
235
+ if is_tty:
236
+ eof_key = "Ctrl+Z (Enter)" if sys.platform == "win32" else "Ctrl+D"
237
+ _cli_print(
238
+ "[mlog-py] Enter Python DSL code below.\n"
239
+ f"[mlog-py] Finish input with EOF ({eof_key}) or type 'EOF' on an empty line:",
240
+ file=sys.stderr,
241
+ )
242
+ lines: List[str] = []
243
+ try:
244
+ while True:
245
+ line = sys.stdin.readline()
246
+ if not line:
247
+ break
248
+ stripped = line.rstrip("\r\n")
249
+ if stripped in ("EOF", "__EOF__"):
250
+ break
251
+ lines.append(line)
252
+ source = "".join(lines)
253
+ except KeyboardInterrupt:
254
+ _cli_print("\n[mlog-py] Compilation aborted by user.", file=sys.stderr)
255
+ return 130
256
+ else:
257
+ try:
258
+ raw_input = sys.stdin.read()
259
+ clean_lines = []
260
+ for line in raw_input.splitlines(True):
261
+ if line.rstrip("\r\n") in ("EOF", "__EOF__"):
262
+ break
263
+ clean_lines.append(line)
264
+ source = "".join(clean_lines)
265
+ except KeyboardInterrupt:
266
+ _cli_print("\n[mlog-py] Compilation aborted by user.", file=sys.stderr)
267
+ return 130
268
+ else:
269
+ filename = input_source
270
+ try:
271
+ with open(input_source, "r", encoding="utf-8") as f:
272
+ source = f.read()
273
+ except OSError as e:
274
+ _cli_print(f"Error reading file '{input_source}': {e}", file=sys.stderr)
275
+ return 1
276
+ except KeyboardInterrupt:
277
+ _cli_print("\n[mlog-py] Compilation aborted by user.", file=sys.stderr)
278
+ return 130
279
+
280
+ if not source.strip():
281
+ _cli_print("Compilation error: input is empty", file=sys.stderr)
282
+ return 1
283
+
284
+ try:
285
+ result = compile_py(
286
+ source=source,
287
+ filename=filename,
288
+ validate=not args.no_validate,
289
+ allow_functions=not args.no_functions,
290
+ fold_constants=not getattr(args, "no_fold_constants", False),
291
+ comments_to_print=getattr(args, "comments_to_print", False),
292
+ )
293
+
294
+ except (CompileError, ValidationError) as e:
295
+ _cli_print(f"Compilation error: {e}", file=sys.stderr)
296
+ return 1
297
+ except KeyboardInterrupt:
298
+ _cli_print("\n[mlog-py] Compilation aborted by user.", file=sys.stderr)
299
+ return 130
300
+
301
+ if args.mindustry:
302
+ from .mindustry_validator import is_mindustry_available, validate_with_mindustry
303
+ if not is_mindustry_available():
304
+ _cli_print("Error: Mindustry engine or Java not available for validation", file=sys.stderr)
305
+ return 1
306
+ try:
307
+ ok, msg, count = validate_with_mindustry(result.mlog)
308
+ except KeyboardInterrupt:
309
+ _cli_print("\n[mlog-py] Compilation aborted by user.", file=sys.stderr)
310
+ return 130
311
+ if not ok:
312
+ _cli_print(f"Mindustry engine validation error: {msg}", file=sys.stderr)
313
+ return 1
314
+
315
+ if args.debug:
316
+ _cli_print("=== IR INSTRUCTIONS (IR index) ===")
317
+ for i, instr in enumerate(result.ir):
318
+ loc_str = f" [{instr.loc.line}:{instr.loc.col}]" if instr.loc else ""
319
+ _cli_print(f" IR[{i:3d}]: {instr}{loc_str}")
320
+ _cli_print("\n=== LABEL TABLE (Resolved instruction address) ===")
321
+ for name, addr in sorted(result.label_table.items()):
322
+ _cli_print(f" {name} -> mlog[{addr}]")
323
+ _cli_print("\n=== FINAL MLOG (Instruction address) ===")
324
+ for idx, line in enumerate(result.mlog.splitlines()):
325
+ _cli_print(f" [{idx:3d}]: {line}")
326
+ _cli_print()
327
+
328
+ output_target = args.output_file
329
+
330
+ if not output_target and is_stdin and is_tty:
331
+ try:
332
+ save_path = _read_interactive_line(
333
+ "[mlog-py] Enter output file path (.mlog) [press Enter to print to stdout]: "
334
+ )
335
+ if save_path:
336
+ output_target = save_path
337
+ except KeyboardInterrupt:
338
+ _cli_print("\n[mlog-py] Compilation aborted by user.", file=sys.stderr)
339
+ return 130
340
+
341
+ if output_target:
342
+ try:
343
+ parent_dir = os.path.dirname(output_target)
344
+ if parent_dir and not os.path.exists(parent_dir):
345
+ os.makedirs(parent_dir, exist_ok=True)
346
+ with open(output_target, "w", encoding="utf-8") as f:
347
+ f.write(result.mlog)
348
+ if not args.output_file:
349
+ _cli_print(f"[mlog-py] Saved compiled bytecode to '{output_target}'.", file=sys.stderr)
350
+ except OSError as e:
351
+ _cli_print(f"Error writing output file '{output_target}': {e}", file=sys.stderr)
352
+ return 1
353
+ elif not args.debug:
354
+ try:
355
+ sys.stdout.write(result.mlog)
356
+ sys.stdout.flush()
357
+ except BrokenPipeError:
358
+ pass
359
+
360
+ # Auto-copy clipboard: enabled by default, disabled if --no-clipboard is set or MLOG_NO_CLIPBOARD=1
361
+ # CLI flag (--clipboard / --no-clipboard) takes precedence over MLOG_NO_CLIPBOARD env var
362
+ if getattr(args, "copy_clipboard", None) is not None:
363
+ should_copy = args.copy_clipboard
364
+ elif os.environ.get("MLOG_NO_CLIPBOARD", "").lower() in ("1", "true", "yes"):
365
+ should_copy = False
366
+ else:
367
+ should_copy = True
368
+
369
+ if should_copy:
370
+ from .clipboard import copy_to_clipboard
371
+ ok, msg = copy_to_clipboard(result.mlog)
372
+ try:
373
+ if ok:
374
+ _cli_print(f"[mlog-py] {msg}", file=sys.stderr)
375
+ else:
376
+ _cli_print(f"[mlog-py] Notice: Could not copy to clipboard.\n{msg}", file=sys.stderr)
377
+ except BrokenPipeError:
378
+ pass
379
+
380
+ return 0
381
+
382
+
383
+ _interactive_prompt_hook: Optional[Callable[[str], str]] = None
384
+
385
+
386
+ def _read_interactive_line(prompt: str) -> str:
387
+ """Prompt user interactively for a single line on stderr, handling TTY and test hooks."""
388
+ _cli_print(prompt, file=sys.stderr, end="", flush=True)
389
+
390
+ # 1. Testing hook takes priority if configured
391
+ global _interactive_prompt_hook
392
+ if _interactive_prompt_hook is not None:
393
+ return _interactive_prompt_hook(prompt)
394
+
395
+ # 2. If sys.stdin is not the real system stdin (e.g. redirected or mocked in test without hook)
396
+ if sys.stdin is not getattr(sys, "__stdin__", sys.stdin):
397
+ return ""
398
+
399
+ # 3. Real interactive terminal: read from controlling terminal device
400
+ try:
401
+ tty_path = "CONIN$" if sys.platform == "win32" else "/dev/tty"
402
+ with open(tty_path, "r", encoding="utf-8", errors="replace") as tty_in:
403
+ line = tty_in.readline()
404
+ return line.strip() if line else ""
405
+ except (KeyboardInterrupt, EOFError):
406
+ raise
407
+ except Exception:
408
+ # Fallback to sys.stdin if terminal device cannot be opened
409
+ try:
410
+ line = sys.stdin.readline()
411
+ return line.strip() if line else ""
412
+ except Exception:
413
+ return ""
414
+
415
+
416
+ def _cli_decompile(args: argparse.Namespace) -> int:
417
+ """Handle decompile subcommand: MLog -> Python DSL."""
418
+ from .decompiler import decompile
419
+ from .decompiler.errors import DecompileError
420
+
421
+ input_source = args.input_file
422
+ is_stdin = (input_source is None or input_source == "-")
423
+
424
+ if is_stdin:
425
+ filename = "<stdin>"
426
+ is_tty = hasattr(sys.stdin, "isatty") and sys.stdin.isatty()
427
+ if is_tty:
428
+ eof_key = "Ctrl+Z (Enter)" if sys.platform == "win32" else "Ctrl+D"
429
+ _cli_print(
430
+ "[mlog-py] Paste or enter mlog code below.\n"
431
+ f"[mlog-py] Finish input with EOF ({eof_key}) or type 'EOF' on an empty line:",
432
+ file=sys.stderr,
433
+ )
434
+ lines: List[str] = []
435
+ try:
436
+ while True:
437
+ line = sys.stdin.readline()
438
+ if not line: # Stream EOF
439
+ break
440
+ stripped = line.rstrip("\r\n")
441
+ if stripped in ("EOF", "__EOF__"):
442
+ break
443
+ lines.append(line)
444
+ mlog_source = "".join(lines)
445
+ except KeyboardInterrupt:
446
+ _cli_print("\n[mlog-py] Decompilation aborted by user.", file=sys.stderr)
447
+ return 130
448
+ else:
449
+ try:
450
+ raw_input = sys.stdin.read()
451
+ clean_lines = []
452
+ for line in raw_input.splitlines(True):
453
+ if line.rstrip("\r\n") in ("EOF", "__EOF__"):
454
+ break
455
+ clean_lines.append(line)
456
+ mlog_source = "".join(clean_lines)
457
+ except KeyboardInterrupt:
458
+ _cli_print("\n[mlog-py] Decompilation aborted by user.", file=sys.stderr)
459
+ return 130
460
+ else:
461
+ filename = input_source
462
+ try:
463
+ with open(input_source, "r", encoding="utf-8") as f:
464
+ mlog_source = f.read()
465
+ except OSError as e:
466
+ _cli_print(f"Error reading file '{input_source}': {e}", file=sys.stderr)
467
+ return 1
468
+ except KeyboardInterrupt:
469
+ _cli_print("\n[mlog-py] Decompilation aborted by user.", file=sys.stderr)
470
+ return 130
471
+
472
+ if not mlog_source.strip():
473
+ _cli_print("Decompilation error: input is empty", file=sys.stderr)
474
+ return 1
475
+
476
+ try:
477
+ python_code = decompile(
478
+ mlog_source,
479
+ filename=filename,
480
+ debug=args.debug,
481
+ )
482
+ except DecompileError as e:
483
+ _cli_print(f"Decompilation error: {e}", file=sys.stderr)
484
+ return 1
485
+ except KeyboardInterrupt:
486
+ _cli_print("\n[mlog-py] Decompilation aborted by user.", file=sys.stderr)
487
+ return 130
488
+
489
+ output_target = args.output_file
490
+
491
+ if not output_target and is_stdin and is_tty:
492
+ try:
493
+ save_path = _read_interactive_line(
494
+ "[mlog-py] Enter output file path (.py) [press Enter to print to stdout]: "
495
+ )
496
+ if save_path:
497
+ output_target = save_path
498
+ except KeyboardInterrupt:
499
+ _cli_print("\n[mlog-py] Decompilation aborted by user.", file=sys.stderr)
500
+ return 130
501
+
502
+ if output_target:
503
+ try:
504
+ parent_dir = os.path.dirname(output_target)
505
+ if parent_dir and not os.path.exists(parent_dir):
506
+ os.makedirs(parent_dir, exist_ok=True)
507
+ with open(output_target, "w", encoding="utf-8") as f:
508
+ f.write(python_code)
509
+ if not args.output_file:
510
+ _cli_print(f"[mlog-py] Saved decompiled code to '{output_target}'.", file=sys.stderr)
511
+ except OSError as e:
512
+ _cli_print(f"Error writing output file '{output_target}': {e}", file=sys.stderr)
513
+ return 1
514
+ else:
515
+ try:
516
+ sys.stdout.write(python_code)
517
+ sys.stdout.flush()
518
+ except BrokenPipeError:
519
+ pass
520
+
521
+ return 0
522
+
523
+
524
+
525
+ def _build_cli_parser() -> argparse.ArgumentParser:
526
+ """Build the argument parser supporting 'compile' and 'decompile' subcommands."""
527
+ prog = "mlog"
528
+ if sys.argv and os.path.basename(sys.argv[0]) in ("mlog", "mlog-py"):
529
+ prog = os.path.basename(sys.argv[0])
530
+
531
+ parser = argparse.ArgumentParser(
532
+ prog=prog,
533
+ description="Compile a controlled Python subset into Mindustry Logic (mlog) or decompile mlog to Python DSL.",
534
+ )
535
+ parser.add_argument(
536
+ "--version",
537
+ "-V",
538
+ action="version",
539
+ version=f"%(prog)s {__version__}",
540
+ )
541
+ subparsers = parser.add_subparsers(
542
+ dest="command",
543
+ title="subcommands",
544
+ metavar="{compile, decompile}",
545
+ help="Subcommands (compile, decompile)",
546
+ )
547
+
548
+ # Subcommand: compile (Python DSL -> MLog)
549
+ compile_p = subparsers.add_parser(
550
+ "compile",
551
+ help="Compile Python DSL -> MLog",
552
+ description="Compile Python DSL into Mindustry Logic (mlog).",
553
+ )
554
+ compile_p.add_argument(
555
+ "input_file",
556
+ nargs="?",
557
+ default=None,
558
+ help="Input Python source file (.py) or '-' / omitted to read from stdin",
559
+ )
560
+ compile_p.add_argument(
561
+ "-o", "--output", dest="output_file", help="Output file path (.mlog)"
562
+ )
563
+ compile_p.add_argument(
564
+ "--debug",
565
+ action="store_true",
566
+ help="Print IR instructions and label table for debugging",
567
+ )
568
+ compile_p.add_argument(
569
+ "--no-validate",
570
+ action="store_true",
571
+ help="Skip mlog syntax validation",
572
+ )
573
+ compile_p.add_argument(
574
+ "--mindustry",
575
+ action="store_true",
576
+ help="Validate generated mlog using Mindustry's actual engine (requires Java)",
577
+ )
578
+ compile_p.add_argument(
579
+ "--no-fold-constants",
580
+ action="store_true",
581
+ help="Disable compile-time constant folding",
582
+ )
583
+ compile_p.add_argument(
584
+ "--no-functions",
585
+ action="store_true",
586
+ help="Reject 'def' function definitions and 'return' statements (disabled by default)",
587
+ )
588
+ compile_p.add_argument(
589
+ "--clipboard",
590
+ dest="copy_clipboard",
591
+ action="store_true",
592
+ default=None,
593
+ help="Copy compiled mlog to system clipboard (enabled by default)",
594
+ )
595
+ compile_p.add_argument(
596
+ "--no-clipboard",
597
+ dest="copy_clipboard",
598
+ action="store_false",
599
+ help="Disable automatic copying of compiled mlog to system clipboard",
600
+ )
601
+ compile_p.add_argument(
602
+ "--comments-to-print",
603
+ dest="comments_to_print",
604
+ action="store_true",
605
+ help="Convert Python comments into Mindustry 'print' instructions",
606
+ )
607
+
608
+
609
+ # Subcommand: decompile (MLog -> Python DSL)
610
+ decompile_p = subparsers.add_parser(
611
+ "decompile",
612
+ help="Decompile MLog -> Python DSL",
613
+ description="Decompile Mindustry Logic (mlog) into structured Python DSL.",
614
+ )
615
+ decompile_p.add_argument(
616
+ "input_file",
617
+ nargs="?",
618
+ default=None,
619
+ help="Input Mindustry Logic file (.mlog) or '-' / omitted to read from stdin",
620
+ )
621
+ decompile_p.add_argument(
622
+ "-o", "--output", dest="output_file", help="Output file path (.py)"
623
+ )
624
+ decompile_p.add_argument(
625
+ "--debug",
626
+ action="store_true",
627
+ help="Include mlog instruction provenance comments (# mlog[<addr>])",
628
+ )
629
+
630
+ return parser
631
+
632
+
633
+ def main(argv: Optional[List[str]] = None) -> int:
634
+ """CLI entry point for Python-to-mlog compiler and mlog-to-Python decompiler."""
635
+ parser = _build_cli_parser()
636
+
637
+ raw_args = list(sys.argv[1:] if argv is None else argv)
638
+
639
+ # When no arguments are provided:
640
+ # If stdin is piped/redirected (not a TTY), default to compiling from stdin
641
+ if not raw_args:
642
+ is_tty = hasattr(sys.stdin, "isatty") and sys.stdin.isatty()
643
+ if not is_tty:
644
+ raw_args = ["compile", "-"]
645
+
646
+ # Backward compatibility:
647
+ # If first argument is not a known subcommand ('compile', 'decompile', '-h', '--help', empty),
648
+ # default to the 'compile' command (e.g. `mlog-py script.py -o script.mlog` or `mlog -`).
649
+ if raw_args and raw_args[0] not in ("compile", "decompile", "-h", "--help", "-V", "--version"):
650
+ raw_args = ["compile"] + raw_args
651
+
652
+ args = parser.parse_args(raw_args)
653
+
654
+ if args.command == "compile":
655
+ return _cli_compile(args)
656
+ elif args.command == "decompile":
657
+ return _cli_decompile(args)
658
+ else:
659
+ parser.print_help(sys.stderr)
660
+ return 1
661
+
662
+
663
+ if __name__ == "__main__":
664
+ sys.exit(main())