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 +664 -0
- mlog/__init__.pyi +2002 -0
- mlog/__main__.py +6 -0
- mlog/allocator.py +108 -0
- mlog/clipboard.py +223 -0
- mlog/comments.py +165 -0
- mlog/compiler.py +4505 -0
- mlog/constants.py +313 -0
- mlog/decompiler/__init__.py +240 -0
- mlog/decompiler/cfg.py +363 -0
- mlog/decompiler/dataflow.py +994 -0
- mlog/decompiler/dsl_symbols.py +245 -0
- mlog/decompiler/errors.py +24 -0
- mlog/decompiler/expression.py +417 -0
- mlog/decompiler/function_analyzer.py +841 -0
- mlog/decompiler/function_ir.py +147 -0
- mlog/decompiler/instruction.py +117 -0
- mlog/decompiler/naming.py +303 -0
- mlog/decompiler/parser.py +197 -0
- mlog/decompiler/semantics.py +422 -0
- mlog/decompiler/sourcemap.py +473 -0
- mlog/decompiler/structured_ir.py +776 -0
- mlog/decompiler/structurer.py +879 -0
- mlog/emitter.py +92 -0
- mlog/errors.py +25 -0
- mlog/extract_registry.py +286 -0
- mlog/ir.py +87 -0
- mlog/metadata.py +3551 -0
- mlog/mindustry_discovery.py +748 -0
- mlog/mindustry_validator.py +744 -0
- mlog/mlog.py +3 -0
- mlog/mlog_registry.py +768 -0
- mlog/mlog_registry_data.py +65 -0
- mlog/optimizer.py +32 -0
- mlog/parser.py +661 -0
- mlog/py.typed +0 -0
- mlog/validator.py +218 -0
- mlog_py-0.8.1.dist-info/METADATA +388 -0
- mlog_py-0.8.1.dist-info/RECORD +43 -0
- mlog_py-0.8.1.dist-info/WHEEL +5 -0
- mlog_py-0.8.1.dist-info/entry_points.txt +3 -0
- mlog_py-0.8.1.dist-info/licenses/LICENSE +21 -0
- mlog_py-0.8.1.dist-info/top_level.txt +1 -0
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())
|