unidecompiler-plugin-lua 0.1.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.
@@ -0,0 +1,1631 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, replace
4
+ import re
5
+
6
+ from unidecompiler.core.ir import (
7
+ Assign,
8
+ BinaryOp,
9
+ Call,
10
+ CapturedVar,
11
+ Const,
12
+ Expr,
13
+ Global,
14
+ GetItem,
15
+ MapLiteral,
16
+ MultiReturn,
17
+ SourceRef,
18
+ StoreItem,
19
+ UnaryOp,
20
+ Var,
21
+ )
22
+ from unidecompiler.core.effects import AssignManyValues, AssignValue, AssignValueOnBranch, Emit, ReturnValues, UnknownOpcode
23
+ from unidecompiler.core.vm_module import assemble_vm_module
24
+ from unidecompiler.core.vm_effect_table import VMEffectTable
25
+ from unidecompiler.core.vm_function import (
26
+ VMFunctionSpec,
27
+ empty_vm_function,
28
+ recover_vm_function,
29
+ lift_steps,
30
+ lift_vm_step_function,
31
+ )
32
+ from unidecompiler.core.vm_bytecode import VMBytecodeStep
33
+ from unidecompiler.core.vm_hints import VMHint
34
+ from unidecompiler.core.vm_operands import VMDecodedInstruction, VMOperand
35
+ from unidecompiler.core.vm_region import (
36
+ VMLinearState,
37
+ VMRegionOpcodeClasses,
38
+ VMRegionProfile,
39
+ VMStatefulCallbacks,
40
+ build_hint_region_profile,
41
+ )
42
+ from unidecompiler_plugin_lua.luac import LuaChunk, LuaFunctionListing
43
+
44
+
45
+ BINARY_OPS = {
46
+ "ADD": "+",
47
+ "SUB": "-",
48
+ "MUL": "*",
49
+ "DIV": "/",
50
+ "IDIV": "//",
51
+ "MOD": "%",
52
+ }
53
+ IMMEDIATE_BINARY_OPS = {
54
+ "ADDI": "+",
55
+ }
56
+ COMPARISON_OPS = {
57
+ "LT": "<",
58
+ "LE": "<=",
59
+ "EQ": "==",
60
+ }
61
+ IMMEDIATE_COMPARISON_OPS = {
62
+ "EQI": "==",
63
+ "LTI": "<",
64
+ "LEI": "<=",
65
+ "GTI": ">",
66
+ "GEI": ">=",
67
+ }
68
+ CONDITIONAL_OPS = frozenset(
69
+ {
70
+ "EQ",
71
+ "LT",
72
+ "LE",
73
+ "EQK",
74
+ "EQI",
75
+ "LTI",
76
+ "LEI",
77
+ "GTI",
78
+ "GEI",
79
+ "TEST",
80
+ "TESTSET",
81
+ }
82
+ )
83
+ JUMP_OPS = frozenset({"JMP", "FORPREP", "FORLOOP", "TFORPREP", "TFORLOOP"})
84
+ CONTROL_OPS = CONDITIONAL_OPS | JUMP_OPS
85
+ LUA_REGION_OPCODE_CLASSES = VMRegionOpcodeClasses(
86
+ control=CONTROL_OPS,
87
+ jumps=JUMP_OPS,
88
+ forward_jumps=JUMP_OPS,
89
+ backward_jumps=JUMP_OPS,
90
+ conditional_jumps=CONDITIONAL_OPS | frozenset({"FORLOOP", "TFORLOOP"}),
91
+ )
92
+ IGNORED_OPS = {
93
+ "MMBIN",
94
+ "MMBINI",
95
+ "MMBINK",
96
+ "VARARGPREP",
97
+ "EXTRAARG",
98
+ "LFALSESKIP",
99
+ "CLOSE",
100
+ "TBC",
101
+ *(CONTROL_OPS - frozenset({"FORPREP", "FORLOOP", "TESTSET", "TFORPREP", "TFORLOOP"})),
102
+ }
103
+ @dataclass(frozen=True)
104
+ class LuaEffectContext:
105
+ listing: LuaFunctionListing
106
+ constants: dict[int, object]
107
+
108
+
109
+ def _lua_no_effect(_context: LuaEffectContext, _instruction, _source: SourceRef) -> tuple:
110
+ return ()
111
+
112
+
113
+ def _lua_unknown_opcode_effect(
114
+ _context: LuaEffectContext,
115
+ instruction,
116
+ source: SourceRef,
117
+ ) -> tuple:
118
+ return (
119
+ UnknownOpcode(
120
+ source=source,
121
+ opcode=instruction.opcode,
122
+ raw=f"{instruction.pc}: {instruction.opcode} {' '.join(instruction.operands)}".strip(),
123
+ ),
124
+ )
125
+
126
+
127
+ def _lua_load_const_value(value):
128
+ def factory(context: LuaEffectContext, instruction, source: SourceRef) -> tuple:
129
+ operands = instruction.operands
130
+ if len(operands) < 1:
131
+ return None
132
+ return (_lua_assign(context.listing, int(operands[0]), instruction.pc, Const(value=value, source=source), source),)
133
+
134
+ return factory
135
+
136
+
137
+ def _lua_load_i(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
138
+ operands = instruction.operands
139
+ if len(operands) < 2:
140
+ return None
141
+ return (_lua_assign(context.listing, int(operands[0]), instruction.pc, Const(value=int(operands[1]), source=source), source),)
142
+
143
+
144
+ def _lua_load_nil(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
145
+ operands = instruction.operands
146
+ if len(operands) < 2:
147
+ return None
148
+ start = int(operands[0])
149
+ count = int(operands[1]) + 1
150
+ return tuple(
151
+ _lua_assign(context.listing, register, instruction.pc, Const(value=None, source=source), source)
152
+ for register in range(start, start + count)
153
+ )
154
+
155
+
156
+ def _lua_load_k(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
157
+ operands = instruction.operands
158
+ if len(operands) < 2:
159
+ return None
160
+ return (_lua_assign(context.listing, int(operands[0]), instruction.pc, Const(value=context.constants.get(int(operands[1])), source=source), source),)
161
+
162
+
163
+ def _lua_move(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
164
+ operands = instruction.operands
165
+ if len(operands) < 2:
166
+ return None
167
+ return (
168
+ _lua_assign(
169
+ context.listing,
170
+ int(operands[0]),
171
+ instruction.pc,
172
+ Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source),
173
+ source,
174
+ ),
175
+ )
176
+
177
+
178
+ def _lua_binary(op: str):
179
+ def factory(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
180
+ operands = instruction.operands
181
+ if len(operands) < 3:
182
+ return None
183
+ return (
184
+ _lua_assign(
185
+ context.listing,
186
+ int(operands[0]),
187
+ instruction.pc,
188
+ BinaryOp(
189
+ source=source,
190
+ op=op,
191
+ left=Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source),
192
+ right=Var(name=_read_register_name(context.listing, int(operands[2]), instruction.pc), source=source),
193
+ semantics="dynamic",
194
+ ),
195
+ source,
196
+ ),
197
+ )
198
+
199
+ return factory
200
+
201
+
202
+ def _lua_unary(op: str):
203
+ def factory(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
204
+ operands = instruction.operands
205
+ if len(operands) < 2:
206
+ return None
207
+ return (
208
+ _lua_assign(
209
+ context.listing,
210
+ int(operands[0]),
211
+ instruction.pc,
212
+ UnaryOp(
213
+ source=source,
214
+ op=op,
215
+ value=Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source),
216
+ ),
217
+ source,
218
+ ),
219
+ )
220
+
221
+ return factory
222
+
223
+
224
+ def _lua_immediate_binary(op: str):
225
+ def factory(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
226
+ operands = instruction.operands
227
+ if len(operands) < 3:
228
+ return None
229
+ return (
230
+ _lua_assign(
231
+ context.listing,
232
+ int(operands[0]),
233
+ instruction.pc,
234
+ BinaryOp(
235
+ source=source,
236
+ op=op,
237
+ left=Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source),
238
+ right=Const(value=int(operands[2]), source=source),
239
+ semantics="dynamic",
240
+ ),
241
+ source,
242
+ ),
243
+ )
244
+
245
+ return factory
246
+
247
+
248
+ def _lua_shift_immediate(op: str, immediate_on_left: bool = False):
249
+ def factory(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
250
+ operands = instruction.operands
251
+ if len(operands) < 3:
252
+ return None
253
+ shift_op = op
254
+ immediate_value = int(operands[2])
255
+ if immediate_value < 0 and op in {"<<", ">>"}:
256
+ shift_op = "<<" if op == ">>" else ">>"
257
+ immediate_value = -immediate_value
258
+ immediate = Const(value=immediate_value, source=source)
259
+ register = Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source)
260
+ left, right = (immediate, register) if immediate_on_left else (register, immediate)
261
+ return (
262
+ _lua_assign(
263
+ context.listing,
264
+ int(operands[0]),
265
+ instruction.pc,
266
+ BinaryOp(source=source, op=shift_op, left=left, right=right, semantics="dynamic"),
267
+ source,
268
+ ),
269
+ )
270
+
271
+ return factory
272
+
273
+
274
+ def _lua_constant_binary(op: str):
275
+ def factory(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
276
+ operands = instruction.operands
277
+ if len(operands) < 3:
278
+ return None
279
+ return (
280
+ _lua_assign(
281
+ context.listing,
282
+ int(operands[0]),
283
+ instruction.pc,
284
+ BinaryOp(
285
+ source=source,
286
+ op=op,
287
+ left=Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source),
288
+ right=Const(value=context.constants.get(int(operands[2])), source=source),
289
+ semantics="dynamic",
290
+ ),
291
+ source,
292
+ ),
293
+ )
294
+
295
+ return factory
296
+
297
+
298
+ def _lua_get_table(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
299
+ operands = instruction.operands
300
+ if len(operands) < 3:
301
+ return None
302
+ return (
303
+ _lua_assign(
304
+ context.listing,
305
+ int(operands[0]),
306
+ instruction.pc,
307
+ GetItem(
308
+ source=source,
309
+ obj=Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source),
310
+ key=Var(name=_read_register_name(context.listing, int(operands[2]), instruction.pc), source=source),
311
+ ),
312
+ source,
313
+ ),
314
+ )
315
+
316
+
317
+ def _lua_get_tabup(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
318
+ operands = instruction.operands
319
+ if len(operands) < 3:
320
+ return None
321
+ key = _constant_operand_expr(context.constants, operands[2], source)
322
+ upvalue = _lua_upvalue_expr(context.listing, operands[1], source)
323
+ if not _is_lua_env_upvalue(upvalue):
324
+ return (
325
+ _lua_assign(
326
+ context.listing,
327
+ int(operands[0]),
328
+ instruction.pc,
329
+ GetItem(source=source, obj=upvalue, key=key),
330
+ source,
331
+ ),
332
+ )
333
+ value: Expr = (
334
+ Global(name=key.value, source=source)
335
+ if isinstance(key, Const) and isinstance(key.value, str)
336
+ else GetItem(source=source, obj=upvalue, key=key)
337
+ )
338
+ return (_lua_assign(context.listing, int(operands[0]), instruction.pc, value, source),)
339
+
340
+
341
+ def _lua_get_field(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
342
+ operands = instruction.operands
343
+ if len(operands) < 3:
344
+ return None
345
+ return (
346
+ _lua_assign(
347
+ context.listing,
348
+ int(operands[0]),
349
+ instruction.pc,
350
+ GetItem(
351
+ source=source,
352
+ obj=Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source),
353
+ key=_constant_operand_expr(context.constants, operands[2], source),
354
+ ),
355
+ source,
356
+ ),
357
+ )
358
+
359
+
360
+ def _lua_get_i(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
361
+ operands = instruction.operands
362
+ if len(operands) < 3:
363
+ return None
364
+ return (
365
+ _lua_assign(
366
+ context.listing,
367
+ int(operands[0]),
368
+ instruction.pc,
369
+ GetItem(
370
+ source=source,
371
+ obj=Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source),
372
+ key=Const(value=int(operands[2]), source=source),
373
+ ),
374
+ source,
375
+ ),
376
+ )
377
+
378
+
379
+ def _lua_set_table(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
380
+ operands = instruction.operands
381
+ if len(operands) < 3:
382
+ return None
383
+ return (
384
+ Emit(
385
+ source=source,
386
+ statement=StoreItem(
387
+ source=source,
388
+ obj=Var(name=_read_register_name(context.listing, int(operands[0]), instruction.pc), source=source),
389
+ key=_operand_expr(context.listing, context.constants, operands[1], instruction.pc, source),
390
+ value=_operand_expr(context.listing, context.constants, operands[2], instruction.pc, source),
391
+ ),
392
+ ),
393
+ )
394
+
395
+
396
+ def _lua_set_tabup(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
397
+ operands = instruction.operands
398
+ if len(operands) < 3:
399
+ return None
400
+ return (
401
+ Emit(
402
+ source=source,
403
+ statement=StoreItem(
404
+ source=source,
405
+ obj=_lua_upvalue_expr(context.listing, operands[0], source),
406
+ key=_constant_operand_expr(context.constants, operands[1], source),
407
+ value=_operand_expr(context.listing, context.constants, operands[2], instruction.pc, source),
408
+ ),
409
+ ),
410
+ )
411
+
412
+
413
+ def _lua_set_i(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
414
+ operands = instruction.operands
415
+ if len(operands) < 3:
416
+ return None
417
+ return (
418
+ Emit(
419
+ source=source,
420
+ statement=StoreItem(
421
+ source=source,
422
+ obj=Var(name=_read_register_name(context.listing, int(operands[0]), instruction.pc), source=source),
423
+ key=Const(value=int(operands[1]), source=source),
424
+ value=_operand_expr(context.listing, context.constants, operands[2], instruction.pc, source),
425
+ ),
426
+ ),
427
+ )
428
+
429
+
430
+ def _lua_set_field(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
431
+ operands = instruction.operands
432
+ if len(operands) < 3:
433
+ return None
434
+ return (
435
+ Emit(
436
+ source=source,
437
+ statement=StoreItem(
438
+ source=source,
439
+ obj=Var(name=_read_register_name(context.listing, int(operands[0]), instruction.pc), source=source),
440
+ key=_constant_operand_expr(context.constants, operands[1], source),
441
+ value=_operand_expr(context.listing, context.constants, operands[2], instruction.pc, source),
442
+ ),
443
+ ),
444
+ )
445
+
446
+
447
+ def _lua_new_table(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
448
+ operands = instruction.operands
449
+ if len(operands) < 1:
450
+ return None
451
+ return (_lua_assign(context.listing, int(operands[0]), instruction.pc, MapLiteral(source=source), source),)
452
+
453
+
454
+ def _lua_self(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
455
+ operands = instruction.operands
456
+ if len(operands) < 3:
457
+ return None
458
+ base = int(operands[0])
459
+ receiver = Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source)
460
+ method = GetItem(
461
+ source=source,
462
+ obj=receiver,
463
+ key=_constant_operand_expr(context.constants, operands[2], source),
464
+ )
465
+ return (
466
+ _lua_assign(context.listing, base + 1, instruction.pc, receiver, source),
467
+ _lua_assign(context.listing, base, instruction.pc, method, source),
468
+ )
469
+
470
+
471
+ def _lua_concat(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
472
+ operands = instruction.operands
473
+ if len(operands) < 2:
474
+ return None
475
+ target = int(operands[0])
476
+ if len(operands) == 2:
477
+ start = target
478
+ end = target + int(operands[1]) - 1
479
+ else:
480
+ start = int(operands[1])
481
+ end = int(operands[2])
482
+ if end < start:
483
+ return None
484
+ expr: Expr = Var(name=_read_register_name(context.listing, start, instruction.pc), source=source)
485
+ for register in range(start + 1, end + 1):
486
+ expr = BinaryOp(
487
+ source=source,
488
+ op="..",
489
+ left=expr,
490
+ right=Var(name=_read_register_name(context.listing, register, instruction.pc), source=source),
491
+ semantics="dynamic",
492
+ )
493
+ return (_lua_assign(context.listing, target, instruction.pc, expr, source),)
494
+
495
+
496
+ def _lua_set_list(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
497
+ operands = instruction.operands
498
+ if len(operands) < 2:
499
+ return None
500
+ base = int(operands[0])
501
+ count = int(operands[1])
502
+ if count <= 0:
503
+ return ()
504
+ table = Var(name=_read_register_name(context.listing, base, instruction.pc), source=source)
505
+ return tuple(
506
+ Emit(
507
+ source=source,
508
+ statement=StoreItem(
509
+ source=source,
510
+ obj=table,
511
+ key=Const(value=index + 1, source=source),
512
+ value=Var(name=_read_register_name(context.listing, base + 1 + index, instruction.pc), source=source),
513
+ ),
514
+ )
515
+ for index in range(count)
516
+ )
517
+
518
+
519
+ def _lua_forloop_effect(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
520
+ operands = instruction.operands
521
+ if not operands:
522
+ return None
523
+ base = int(operands[0])
524
+ index_name = _read_register_name(context.listing, base, instruction.pc)
525
+ step_name = _read_register_name(context.listing, base + 2, instruction.pc)
526
+ body_target = _lua_jump_target(instruction)
527
+ visible_index_name = _write_register_name(
528
+ context.listing,
529
+ base + 3,
530
+ body_target if body_target is not None else instruction.pc,
531
+ )
532
+ next_index = BinaryOp(
533
+ source=source,
534
+ op="+",
535
+ left=Var(name=index_name, source=source),
536
+ right=Var(name=step_name, source=source),
537
+ semantics="dynamic",
538
+ )
539
+ return (
540
+ AssignValue(
541
+ source=source,
542
+ name=index_name,
543
+ target=Var(name=index_name, source=source),
544
+ value=next_index,
545
+ ),
546
+ AssignValue(
547
+ source=source,
548
+ name=visible_index_name,
549
+ target=Var(name=visible_index_name, source=source),
550
+ value=Var(name=index_name, source=source),
551
+ ),
552
+ )
553
+
554
+
555
+ def _lua_forprep_effect(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
556
+ operands = instruction.operands
557
+ if not operands:
558
+ return None
559
+ base = int(operands[0])
560
+ index_name = _read_register_name(context.listing, base, instruction.pc)
561
+ step_name = _read_register_name(context.listing, base + 2, instruction.pc)
562
+ prepared_index = BinaryOp(
563
+ source=source,
564
+ op="-",
565
+ left=Var(name=index_name, source=source),
566
+ right=Var(name=step_name, source=source),
567
+ semantics="dynamic",
568
+ )
569
+ return (
570
+ AssignValue(
571
+ source=source,
572
+ name=index_name,
573
+ target=Var(name=index_name, source=source),
574
+ value=prepared_index,
575
+ ),
576
+ )
577
+
578
+
579
+ def _lua_testset_effect(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
580
+ operands = instruction.operands
581
+ if len(operands) < 2:
582
+ return None
583
+ target_name = _write_register_name(context.listing, int(operands[0]), instruction.pc)
584
+ value = Var(name=_read_register_name(context.listing, int(operands[1]), instruction.pc), source=source)
585
+ return (
586
+ AssignValueOnBranch(
587
+ source=source,
588
+ name=target_name,
589
+ target=Var(name=target_name, source=source),
590
+ value=value,
591
+ branch="false",
592
+ ),
593
+ )
594
+
595
+
596
+ def _lua_tforcall_effect(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
597
+ operands = instruction.operands
598
+ if len(operands) < 3:
599
+ return None
600
+ base = int(operands[0])
601
+ count = int(operands[2])
602
+ if count <= 0:
603
+ return ()
604
+ names = tuple(
605
+ _write_register_name(context.listing, register, instruction.pc)
606
+ for register in range(base + 4, base + 4 + count)
607
+ )
608
+ return (
609
+ AssignManyValues(
610
+ source=source,
611
+ names=names,
612
+ targets=tuple(Var(name=name, source=source) for name in names),
613
+ values=(
614
+ MultiReturn(
615
+ source=source,
616
+ value=Call(
617
+ source=source,
618
+ callee=Global(name="lua_generic_for_next", source=source),
619
+ args=(
620
+ Var(name=_read_register_name(context.listing, base, instruction.pc), source=source),
621
+ Var(name=_read_register_name(context.listing, base + 1, instruction.pc), source=source),
622
+ Var(name=_read_register_name(context.listing, base + 2, instruction.pc), source=source),
623
+ ),
624
+ ),
625
+ ),
626
+ ),
627
+ ),
628
+ )
629
+
630
+
631
+ def _lua_tforloop_effect(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
632
+ operands = instruction.operands
633
+ if not operands:
634
+ return None
635
+ base = int(operands[0])
636
+ control_name = _write_register_name(context.listing, base + 2, instruction.pc)
637
+ value_name = _read_register_name(context.listing, base + 4, instruction.pc)
638
+ return (
639
+ AssignValueOnBranch(
640
+ source=source,
641
+ name=control_name,
642
+ target=Var(name=control_name, source=source),
643
+ value=Var(name=value_name, source=source),
644
+ branch="true",
645
+ ),
646
+ )
647
+
648
+
649
+ def _lua_vararg(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
650
+ operands = instruction.operands
651
+ if len(operands) < 2:
652
+ return None
653
+ base = int(operands[0])
654
+ count = int(operands[1]) - 1
655
+ varargs = Call(source=source, callee=Global(name="varargs", source=source), returns="unknown")
656
+ if count <= 0:
657
+ return (_lua_assign(context.listing, base, instruction.pc, varargs, source),)
658
+ return tuple(
659
+ _lua_assign(
660
+ context.listing,
661
+ base + index,
662
+ instruction.pc,
663
+ GetItem(source=source, obj=varargs, key=Const(value=index, source=source)),
664
+ source,
665
+ )
666
+ for index in range(count)
667
+ )
668
+
669
+
670
+ def _lua_closure(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
671
+ operands = instruction.operands
672
+ if len(operands) < 2:
673
+ return None
674
+ function_index = int(operands[1])
675
+ function_name = (
676
+ context.listing.child_function_names[function_index]
677
+ if 0 <= function_index < len(context.listing.child_function_names)
678
+ else f"<function_{function_index}>"
679
+ )
680
+ return (
681
+ _lua_assign(
682
+ context.listing,
683
+ int(operands[0]),
684
+ instruction.pc,
685
+ Global(name=function_name, source=source),
686
+ source,
687
+ ),
688
+ )
689
+
690
+
691
+ def _lua_call(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
692
+ operands = instruction.operands
693
+ if len(operands) < 3:
694
+ return None
695
+ base = int(operands[0])
696
+ return_count = int(operands[2]) - 1
697
+ arg_count = int(operands[1]) - 1
698
+ if arg_count < 0:
699
+ arg_count = _lua_open_call_arg_count(context.listing, instruction)
700
+ args = tuple(
701
+ _lua_call_argument_expr(context.listing, register, instruction.pc, source)
702
+ for register in range(base + 1, base + 1 + arg_count)
703
+ )
704
+ call = Call(
705
+ source=source,
706
+ callee=Var(name=_read_register_name(context.listing, base, instruction.pc), source=source),
707
+ args=args,
708
+ returns=return_count if return_count >= 0 else "unknown",
709
+ )
710
+ if return_count == 0:
711
+ if instruction.opcode == "TAILCALL":
712
+ return (ReturnValues(source=source, values=(call,)),)
713
+ return (_lua_assign(context.listing, base, instruction.pc, call, source),)
714
+ if return_count > 1:
715
+ names = tuple(
716
+ _write_register_name(context.listing, register, instruction.pc)
717
+ for register in range(base, base + return_count)
718
+ )
719
+ return (
720
+ AssignManyValues(
721
+ source=source,
722
+ names=names,
723
+ targets=tuple(Var(name=name, source=source) for name in names),
724
+ values=(MultiReturn(source=source, value=call),),
725
+ ),
726
+ )
727
+ return (_lua_assign(context.listing, base, instruction.pc, call, source),)
728
+
729
+
730
+ def _lua_return1(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
731
+ operands = instruction.operands
732
+ if len(operands) < 1:
733
+ return None
734
+ return (
735
+ ReturnValues(
736
+ source=source,
737
+ values=(Var(name=_read_register_name(context.listing, int(operands[0]), instruction.pc), source=source),),
738
+ ),
739
+ )
740
+
741
+
742
+ def _lua_return_with_base_count(default_count: int):
743
+ def factory(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
744
+ operands = instruction.operands
745
+ if len(operands) < 1:
746
+ return None
747
+ start = int(operands[0])
748
+ return (
749
+ ReturnValues(
750
+ source=source,
751
+ values=tuple(
752
+ Var(name=_read_register_name(context.listing, register, instruction.pc), source=source)
753
+ for register in range(start, start + default_count)
754
+ ),
755
+ ),
756
+ )
757
+
758
+ return factory
759
+
760
+
761
+ def _lua_return(context: LuaEffectContext, instruction, source: SourceRef) -> tuple | None:
762
+ operands = instruction.operands
763
+ if len(operands) < 1:
764
+ return None
765
+ start = int(operands[0])
766
+ count = int(operands[1]) - 1 if len(operands) >= 2 else 1
767
+ if count < 0:
768
+ count = 1
769
+ return (
770
+ ReturnValues(
771
+ source=source,
772
+ values=tuple(
773
+ Var(name=_read_register_name(context.listing, register, instruction.pc), source=source)
774
+ for register in range(start, start + count)
775
+ ),
776
+ ),
777
+ )
778
+
779
+
780
+ LUA_EFFECT_TABLE = VMEffectTable(
781
+ opcode_attr="opcode",
782
+ ignored=frozenset(IGNORED_OPS),
783
+ exact={
784
+ "LOADI": _lua_load_i,
785
+ "LOADF": _lua_load_i,
786
+ "LOADTRUE": _lua_load_const_value(True),
787
+ "LOADFALSE": _lua_load_const_value(False),
788
+ "LOADNIL": _lua_load_nil,
789
+ "LOADK": _lua_load_k,
790
+ "LOADKX": _lua_load_k,
791
+ "MOVE": _lua_move,
792
+ "GETUPVAL": lambda context, instruction, source: (
793
+ _lua_assign(
794
+ context.listing,
795
+ int(instruction.operands[0]),
796
+ instruction.pc,
797
+ _lua_upvalue_expr(context.listing, instruction.operands[1], source),
798
+ source,
799
+ ),
800
+ ) if len(instruction.operands) >= 2 else None,
801
+ "SETUPVAL": lambda context, instruction, source: (
802
+ Emit(
803
+ source=source,
804
+ statement=Assign(
805
+ source=source,
806
+ target=_lua_upvalue_expr(context.listing, instruction.operands[1], source),
807
+ value=Var(
808
+ name=_read_register_name(context.listing, int(instruction.operands[0]), instruction.pc),
809
+ source=source,
810
+ ),
811
+ ),
812
+ ),
813
+ ) if len(instruction.operands) >= 2 else None,
814
+ "UNM": _lua_unary("-"),
815
+ "BNOT": _lua_unary("~"),
816
+ "NOT": _lua_unary("not "),
817
+ "LEN": _lua_unary("#"),
818
+ **{opcode: _lua_binary(op) for opcode, op in BINARY_OPS.items()},
819
+ **{opcode: _lua_immediate_binary(op) for opcode, op in IMMEDIATE_BINARY_OPS.items()},
820
+ **{f"{opcode}K": _lua_constant_binary(op) for opcode, op in BINARY_OPS.items()},
821
+ "POW": _lua_binary("^"),
822
+ "BAND": _lua_binary("&"),
823
+ "BOR": _lua_binary("|"),
824
+ "BXOR": _lua_binary("~"),
825
+ "SHL": _lua_binary("<<"),
826
+ "SHR": _lua_binary(">>"),
827
+ "POWK": _lua_constant_binary("^"),
828
+ "BANDK": _lua_constant_binary("&"),
829
+ "BORK": _lua_constant_binary("|"),
830
+ "BXORK": _lua_constant_binary("~"),
831
+ "SHRI": _lua_shift_immediate(">>"),
832
+ "SHLI": _lua_shift_immediate("<<", immediate_on_left=True),
833
+ "GETTABLE": _lua_get_table,
834
+ "GETTABUP": _lua_get_tabup,
835
+ "GETFIELD": _lua_get_field,
836
+ "GETI": _lua_get_i,
837
+ "SETTABUP": _lua_set_tabup,
838
+ "SETTABLE": _lua_set_table,
839
+ "SETFIELD": _lua_set_field,
840
+ "SETI": _lua_set_i,
841
+ "NEWTABLE": _lua_new_table,
842
+ "SELF": _lua_self,
843
+ "CONCAT": _lua_concat,
844
+ "SETLIST": _lua_set_list,
845
+ "VARARG": _lua_vararg,
846
+ "CLOSURE": _lua_closure,
847
+ "CALL": _lua_call,
848
+ "TAILCALL": _lua_call,
849
+ "FORPREP": _lua_forprep_effect,
850
+ "FORLOOP": _lua_forloop_effect,
851
+ "TFORPREP": _lua_no_effect,
852
+ "TFORCALL": _lua_tforcall_effect,
853
+ "TFORLOOP": _lua_tforloop_effect,
854
+ "TESTSET": _lua_testset_effect,
855
+ "RETURN1": _lua_return1,
856
+ "RETURN0": lambda _context, _instruction, source: (ReturnValues(source=source, values=()),),
857
+ "RETURN2": _lua_return_with_base_count(2),
858
+ "RETURNI": _lua_return_with_base_count(1),
859
+ "RETURNK": _lua_return_with_base_count(1),
860
+ "RETURN": _lua_return,
861
+ },
862
+ fallback=_lua_unknown_opcode_effect,
863
+ )
864
+
865
+
866
+ def lift_lua_chunk(chunk: LuaChunk, metadata: dict) -> ModuleIR:
867
+ functions: tuple[FunctionIR, ...]
868
+ if chunk.functions:
869
+ root, next_index = _lift_lua_function_tree(chunk.functions, 0)
870
+ if next_index != len(chunk.functions):
871
+ raise ValueError("incomplete Lua function tree reconstruction")
872
+ functions = (root,)
873
+ else:
874
+ functions = ()
875
+
876
+ if not functions:
877
+ functions = (
878
+ empty_vm_function(VMFunctionSpec(name="<chunk>", params=(), frontend="lua", instruction_count=0)),
879
+ )
880
+
881
+ return assemble_vm_module(
882
+ name=chunk.filename or "<lua-chunk>",
883
+ source_language="lua",
884
+ metadata={
885
+ "frontend": metadata,
886
+ "bytecode_format": "luac",
887
+ "lua_version": chunk.header.version_label,
888
+ "lua_disassembly": chunk.disassembly,
889
+ },
890
+ functions=functions,
891
+ )
892
+
893
+
894
+ def _lift_lua_function_tree(
895
+ listings: tuple[LuaFunctionListing, ...],
896
+ index: int,
897
+ ) -> tuple[FunctionIR, int]:
898
+ listing = listings[index]
899
+ function_ir = recover_vm_function(
900
+ _lua_function_spec(listing),
901
+ lambda: lift_lua_function(listing),
902
+ raw=tuple(
903
+ f"{instruction.pc}: {instruction.opcode} {' '.join(instruction.operands)}".strip()
904
+ for instruction in listing.instructions
905
+ ),
906
+ )
907
+ next_index = index + 1
908
+ nested_functions: list[FunctionIR] = []
909
+ for _ in range(listing.child_function_count):
910
+ nested_function, next_index = _lift_lua_function_tree(listings, next_index)
911
+ nested_functions.append(nested_function)
912
+ return replace(function_ir, nested_functions=tuple(nested_functions)), next_index
913
+
914
+
915
+ def lift_lua_function(listing: LuaFunctionListing) -> FunctionIR | None:
916
+ constants = {constant.index: constant.value for constant in listing.constants}
917
+ local_names = {
918
+ local.name
919
+ for local in listing.locals
920
+ if local.slot >= listing.param_count and _looks_user_named(local.name)
921
+ }
922
+ steps = tuple(_lua_bytecode_step(listing, constants, instruction) for instruction in listing.instructions)
923
+ return lift_vm_step_function(
924
+ _lua_function_spec(listing, tuple(sorted(local_names))),
925
+ steps,
926
+ profile=_lua_region_profile(steps, listing.instructions),
927
+ stateful_callbacks=VMStatefulCallbacks(
928
+ initial_locals=lambda: {},
929
+ lift_linear=lambda start, end, locals, stack: _lua_linear_state(
930
+ listing,
931
+ constants,
932
+ listing.instructions,
933
+ start,
934
+ end,
935
+ locals,
936
+ stack,
937
+ ),
938
+ branch_condition=lambda branch, stack: _lua_branch_condition(
939
+ _lua_instruction_for_step(listing.instructions, branch),
940
+ constants,
941
+ listing,
942
+ stack,
943
+ ),
944
+ branch_stack_width=lambda branch: _lua_branch_stack_width(_lua_instruction_for_step(listing.instructions, branch)),
945
+ ),
946
+ raw_window=lambda index: _lua_raw_instruction_window(listing.instructions, index),
947
+ )
948
+
949
+
950
+ def _lua_region_profile(
951
+ steps: tuple[VMBytecodeStep, ...],
952
+ instructions: tuple[object, ...],
953
+ ) -> VMRegionProfile[VMBytecodeStep]:
954
+ return build_hint_region_profile(
955
+ steps,
956
+ frontend="lua",
957
+ opcode_classes=LUA_REGION_OPCODE_CLASSES,
958
+ raw_window=lambda index: _lua_raw_instruction_window(instructions, index),
959
+ )
960
+
961
+
962
+ def _lua_linear_state(
963
+ listing: LuaFunctionListing,
964
+ constants: dict[int, object],
965
+ instructions: tuple[object, ...],
966
+ start: int,
967
+ end: int,
968
+ initial_locals: dict[str, Expr],
969
+ initial_stack: tuple[Expr, ...],
970
+ ) -> VMLinearState | None:
971
+ steps = tuple(_lua_bytecode_step(listing, constants, instruction) for instruction in instructions[start:end])
972
+ result = lift_steps(steps, initial_locals=initial_locals, initial_stack=initial_stack)
973
+ if result.state.diagnostics:
974
+ return None
975
+ if result.stopped_at is not None and result.state.terminator is None:
976
+ return None
977
+ if result.state.terminator is not None and (
978
+ result.stopped_at is None
979
+ or result.stopped_at.source.offset != steps[-1].source.offset
980
+ ):
981
+ return None
982
+ return VMLinearState(
983
+ locals=result.state.locals,
984
+ stack=tuple(result.state.stack),
985
+ statements=tuple(result.state.statements),
986
+ terminator=result.state.terminator,
987
+ )
988
+
989
+
990
+ def _lua_instruction_for_step(instructions: tuple[object, ...], step: VMBytecodeStep):
991
+ for instruction in instructions:
992
+ if instruction.pc == step.source.offset:
993
+ return instruction
994
+ return instructions[0]
995
+
996
+
997
+ def _lua_branch_stack_width(_instruction) -> int:
998
+ return 0
999
+
1000
+
1001
+ def _lua_branch_condition(
1002
+ instruction,
1003
+ constants: dict[int, object],
1004
+ listing: LuaFunctionListing,
1005
+ _stack: tuple[Expr, ...],
1006
+ ) -> Expr | None:
1007
+ source = SourceRef(frontend="lua", offset=instruction.pc, line=instruction.line)
1008
+ operands = instruction.operands
1009
+ if instruction.opcode in COMPARISON_OPS and len(operands) >= 3:
1010
+ condition: Expr = BinaryOp(
1011
+ source=source,
1012
+ op=COMPARISON_OPS[instruction.opcode],
1013
+ left=Var(name=_read_register_name(listing, int(operands[0]), instruction.pc), source=source),
1014
+ right=Var(name=_read_register_name(listing, int(operands[1]), instruction.pc), source=source),
1015
+ semantics="dynamic",
1016
+ )
1017
+ if len(operands) >= 3 and int(operands[2]) != 0:
1018
+ condition = UnaryOp(source=source, op="not ", value=condition)
1019
+ return condition
1020
+ if instruction.opcode == "EQK" and len(operands) >= 2:
1021
+ condition = BinaryOp(
1022
+ source=source,
1023
+ op="==",
1024
+ left=Var(name=_read_register_name(listing, int(operands[0]), instruction.pc), source=source),
1025
+ right=_constant_operand_expr(constants, operands[1], source),
1026
+ semantics="dynamic",
1027
+ )
1028
+ if len(operands) >= 3 and int(operands[2]) != 0:
1029
+ condition = UnaryOp(source=source, op="not ", value=condition)
1030
+ return condition
1031
+ if instruction.opcode in IMMEDIATE_COMPARISON_OPS and len(operands) >= 2:
1032
+ condition = BinaryOp(
1033
+ source=source,
1034
+ op=IMMEDIATE_COMPARISON_OPS[instruction.opcode],
1035
+ left=Var(name=_read_register_name(listing, int(operands[0]), instruction.pc), source=source),
1036
+ right=Const(value=int(operands[1]), source=source),
1037
+ semantics="dynamic",
1038
+ )
1039
+ if len(operands) >= 3 and int(operands[2]) != 0:
1040
+ condition = UnaryOp(source=source, op="not ", value=condition)
1041
+ return condition
1042
+ if instruction.opcode == "TESTSET" and len(operands) >= 2:
1043
+ value = Var(name=_read_register_name(listing, int(operands[1]), instruction.pc), source=source)
1044
+ if len(operands) >= 3 and int(operands[2]) != 0:
1045
+ return UnaryOp(source=source, op="not ", value=value)
1046
+ return value
1047
+ if instruction.opcode == "TEST" and operands:
1048
+ value = Var(name=_read_register_name(listing, int(operands[0]), instruction.pc), source=source)
1049
+ if len(operands) >= 2 and int(operands[1]) != 0:
1050
+ return UnaryOp(source=source, op="not ", value=value)
1051
+ return value
1052
+ if instruction.opcode == "FORLOOP" and operands:
1053
+ base = int(operands[0])
1054
+ index = Var(name=_read_register_name(listing, base, instruction.pc), source=source)
1055
+ limit = Var(name=_read_register_name(listing, base + 1, instruction.pc), source=source)
1056
+ step = Var(name=_read_register_name(listing, base + 2, instruction.pc), source=source)
1057
+ return Call(
1058
+ source=source,
1059
+ callee=Global(name="vm_forloop_continues", source=source),
1060
+ args=(index, limit, step),
1061
+ )
1062
+ if instruction.opcode == "TFORLOOP" and operands:
1063
+ base = int(operands[0])
1064
+ return BinaryOp(
1065
+ source=source,
1066
+ op="!=",
1067
+ left=Var(name=_read_register_name(listing, base + 4, instruction.pc), source=source),
1068
+ right=Const(value=None, source=source),
1069
+ semantics="dynamic",
1070
+ )
1071
+ return None
1072
+
1073
+
1074
+ def _local_for_slot_at_pc(
1075
+ listing: LuaFunctionListing,
1076
+ slot: int,
1077
+ pc: int,
1078
+ ):
1079
+ for local in listing.locals:
1080
+ if local.slot == slot and local.start_pc <= pc < local.end_pc:
1081
+ return local
1082
+ return None
1083
+
1084
+
1085
+ def _lua_bytecode_step(
1086
+ listing: LuaFunctionListing,
1087
+ constants: dict[int, object],
1088
+ instruction,
1089
+ ) -> VMBytecodeStep:
1090
+ source = SourceRef(frontend="lua", offset=instruction.pc, line=instruction.line, detail=f"pc={instruction.pc}")
1091
+ decoded = _lua_decoded_instruction(instruction, source)
1092
+ return VMBytecodeStep(
1093
+ opcode=decoded.opcode,
1094
+ source=source,
1095
+ effects=_lua_instruction_effects(listing, constants, instruction, source),
1096
+ raw=decoded.raw,
1097
+ decoded=decoded,
1098
+ hints=_lua_instruction_hints(listing, instruction, source),
1099
+ )
1100
+
1101
+
1102
+ def _lua_decoded_instruction(instruction, source: SourceRef) -> VMDecodedInstruction:
1103
+ return VMDecodedInstruction(
1104
+ opcode=instruction.opcode,
1105
+ source=source,
1106
+ operands=tuple(VMOperand(role=_lua_operand_role(operand), value=operand, text=operand) for operand in instruction.operands),
1107
+ raw=f"{instruction.pc}: {instruction.opcode} {' '.join(instruction.operands)}".strip(),
1108
+ )
1109
+
1110
+
1111
+ def _lua_operand_role(operand: str):
1112
+ if operand.endswith("k"):
1113
+ return "constant"
1114
+ if operand.lstrip("-").isdigit():
1115
+ return "register"
1116
+ return "raw"
1117
+
1118
+
1119
+ def _lua_instruction_hints(listing: LuaFunctionListing, instruction, source: SourceRef) -> tuple[VMHint, ...]:
1120
+ if instruction.opcode in CONDITIONAL_OPS:
1121
+ return (
1122
+ VMHint(
1123
+ kind="branch-target",
1124
+ source=source,
1125
+ target=instruction.pc + 2,
1126
+ label=instruction.opcode,
1127
+ detail="target-if-true",
1128
+ flow="conditional",
1129
+ ),
1130
+ )
1131
+ if instruction.opcode not in JUMP_OPS:
1132
+ return ()
1133
+ if instruction.opcode == "TFORLOOP":
1134
+ target = _lua_tforloop_body_target(listing, instruction)
1135
+ if target is None:
1136
+ return ()
1137
+ return (
1138
+ VMHint(
1139
+ kind="loop-backedge",
1140
+ source=source,
1141
+ target=target,
1142
+ label=instruction.opcode,
1143
+ detail="target-if-true",
1144
+ flow="conditional",
1145
+ ),
1146
+ )
1147
+ target = _lua_jump_target(instruction)
1148
+ kind = "loop-backedge" if target is not None and target <= instruction.pc else "branch-target"
1149
+ detail = "target-if-true" if instruction.opcode == "FORLOOP" else None
1150
+ flow = "conditional" if instruction.opcode in {"FORLOOP", "TFORLOOP"} else "unconditional"
1151
+ return (VMHint(kind=kind, source=source, target=target, label=instruction.opcode, detail=detail, flow=flow),)
1152
+
1153
+
1154
+ def _lua_tforloop_body_target(listing: LuaFunctionListing, instruction) -> int | None:
1155
+ if not instruction.operands:
1156
+ return None
1157
+ base = instruction.operands[0]
1158
+ for prior in reversed(tuple(candidate for candidate in listing.instructions if candidate.pc < instruction.pc)):
1159
+ if prior.opcode == "TFORPREP" and prior.operands and prior.operands[0] == base:
1160
+ return prior.pc + 1
1161
+ return None
1162
+
1163
+
1164
+ def _lua_comment_target(instruction) -> int | None:
1165
+ comment = getattr(instruction, "comment", None)
1166
+ if not comment:
1167
+ return None
1168
+ match = re.search(r"\bto\s+(-?\d+)\b", comment)
1169
+ if match is None:
1170
+ return None
1171
+ return int(match.group(1))
1172
+
1173
+
1174
+ def _lua_jump_target(instruction) -> int | None:
1175
+ target = _lua_comment_target(instruction)
1176
+ if target is not None or not instruction.operands:
1177
+ return target
1178
+ try:
1179
+ return instruction.pc + 1 + int(instruction.operands[-1])
1180
+ except ValueError:
1181
+ return None
1182
+
1183
+
1184
+ def _lua_function_spec(listing: LuaFunctionListing, local_names: tuple[str, ...] = ()) -> VMFunctionSpec:
1185
+ return VMFunctionSpec(
1186
+ name=listing.inferred_name or "<function>",
1187
+ params=tuple(_parameter_name(listing, index) for index in range(listing.param_count)),
1188
+ frontend="lua",
1189
+ instruction_count=len(listing.instructions),
1190
+ local_names=local_names,
1191
+ )
1192
+
1193
+
1194
+ def _lua_upvalue_expr(listing: LuaFunctionListing, index: object, source: SourceRef) -> CapturedVar:
1195
+ upvalue_index = int(index)
1196
+ if 0 <= upvalue_index < len(listing.upvalues):
1197
+ name = listing.upvalues[upvalue_index].name
1198
+ if name:
1199
+ return CapturedVar(name=name, source=source)
1200
+ return CapturedVar(name=f"upvalue_{index}", source=source)
1201
+
1202
+
1203
+ def _is_lua_env_upvalue(expr: Expr) -> bool:
1204
+ return isinstance(expr, CapturedVar) and expr.name == "_ENV"
1205
+
1206
+
1207
+ def _lua_instruction_effects(
1208
+ listing: LuaFunctionListing,
1209
+ constants: dict[int, object],
1210
+ instruction,
1211
+ source: SourceRef,
1212
+ ) -> tuple[object, ...] | None:
1213
+ return LUA_EFFECT_TABLE.effects_for(LuaEffectContext(listing=listing, constants=constants), instruction, source)
1214
+
1215
+
1216
+ def _lua_assign(
1217
+ listing: LuaFunctionListing,
1218
+ register: int,
1219
+ pc: int,
1220
+ value: Expr,
1221
+ source: SourceRef,
1222
+ ) -> AssignValue:
1223
+ target_name = _write_register_name(listing, register, pc)
1224
+ return AssignValue(
1225
+ source=source,
1226
+ name=target_name,
1227
+ target=Var(name=target_name, source=source),
1228
+ value=value,
1229
+ )
1230
+
1231
+
1232
+ def _local_starting_near_write(
1233
+ listing: LuaFunctionListing,
1234
+ slot: int,
1235
+ pc: int,
1236
+ ):
1237
+ current = _local_for_slot_at_pc(listing, slot, pc)
1238
+ if current is not None and current.end_pc <= pc:
1239
+ for local in listing.locals:
1240
+ if local.slot == slot and local.start_pc == pc + 1 and _looks_user_named(local.name):
1241
+ return local
1242
+ if current is not None and _looks_user_named(current.name):
1243
+ shadowing = (
1244
+ local
1245
+ for local in listing.locals
1246
+ if local.slot == slot
1247
+ and local.start_pc <= pc < local.end_pc
1248
+ and _looks_user_named(local.name)
1249
+ )
1250
+ return max(shadowing, key=lambda local: local.start_pc, default=current)
1251
+ active = _local_for_slot_at_pc(listing, slot, pc + 1)
1252
+ if active is not None:
1253
+ return active
1254
+ for lookahead_pc in (pc + 1, pc + 2):
1255
+ for local in listing.locals:
1256
+ if local.slot == slot and local.start_pc == lookahead_pc and _looks_user_named(local.name):
1257
+ return local
1258
+ table_local = _local_starting_after_table_initializer(listing, slot, pc)
1259
+ if table_local is not None:
1260
+ return table_local
1261
+ if current is not None:
1262
+ return current
1263
+ for lookahead_pc in (pc + 1, pc + 2):
1264
+ for local in listing.locals:
1265
+ if local.slot == slot and local.start_pc == lookahead_pc:
1266
+ return local
1267
+ return None
1268
+
1269
+
1270
+ def _parameter_name(listing: LuaFunctionListing, register: int) -> str:
1271
+ local = _local_for_slot_at_pc(listing, register, 1)
1272
+ if local is not None:
1273
+ return _local_display_name(local, register)
1274
+ return f"arg{register}"
1275
+
1276
+
1277
+ def _read_register_name(listing: LuaFunctionListing, register: int, pc: int) -> str:
1278
+ local = _local_for_slot_at_pc(listing, register, pc)
1279
+ if local is not None:
1280
+ return _local_display_name(local, register)
1281
+ initializer_local = _table_initializer_local_for_slot_at_pc(listing, register, pc)
1282
+ if initializer_local is not None:
1283
+ return _local_display_name(initializer_local, register)
1284
+ initializer_local = _local_starting_after_setlist_read(listing, register, pc)
1285
+ if initializer_local is not None:
1286
+ return _local_display_name(initializer_local, register)
1287
+ if register < listing.param_count:
1288
+ return _parameter_name(listing, register)
1289
+ return f"r{register}"
1290
+
1291
+
1292
+ def _write_register_name(listing: LuaFunctionListing, register: int, pc: int) -> str:
1293
+ local = _local_starting_near_write(listing, register, pc)
1294
+ if local is not None:
1295
+ if local.start_pc > pc + 1 and _next_instruction_reads_register(listing, pc, register):
1296
+ return f"r{register}"
1297
+ return _local_display_name(local, register)
1298
+ if register < listing.param_count:
1299
+ return _parameter_name(listing, register)
1300
+ return f"r{register}"
1301
+
1302
+
1303
+ def _local_display_name(local, register: int) -> str:
1304
+ if _looks_user_named(local.name):
1305
+ return local.name
1306
+ return f"r{register}"
1307
+
1308
+
1309
+ def _local_starting_after_table_initializer(
1310
+ listing: LuaFunctionListing,
1311
+ slot: int,
1312
+ pc: int,
1313
+ ):
1314
+ instruction = _instruction_at_pc(listing, pc)
1315
+ if instruction is None or instruction.opcode != "NEWTABLE" or not instruction.operands:
1316
+ return None
1317
+ if int(instruction.operands[0]) != slot:
1318
+ return None
1319
+ # Debug-local ranges begin after the final initializer instruction. A
1320
+ # table literal can contain more than the old fixed lookahead window, so
1321
+ # find that boundary from bytecode facts instead of guessing a source-size
1322
+ # limit. Values for SETLIST may be prepared in other registers, but the
1323
+ # table register itself must never be overwritten or cross a control-flow
1324
+ # boundary before its debug-local range starts.
1325
+ saw_table_write = False
1326
+ for candidate_instruction in listing.instructions:
1327
+ if candidate_instruction.pc <= pc:
1328
+ continue
1329
+ local = next(
1330
+ (
1331
+ item
1332
+ for item in listing.locals
1333
+ if item.slot == slot
1334
+ and item.start_pc == candidate_instruction.pc
1335
+ and _looks_user_named(item.name)
1336
+ ),
1337
+ None,
1338
+ )
1339
+ if local is not None:
1340
+ return local if saw_table_write else None
1341
+ if _is_table_initializer_write_for_slot(candidate_instruction, slot):
1342
+ saw_table_write = True
1343
+ continue
1344
+ if candidate_instruction.opcode in CONTROL_OPS:
1345
+ return None
1346
+ if _instruction_writes_register(candidate_instruction, slot):
1347
+ return None
1348
+ return None
1349
+
1350
+
1351
+ def _is_table_initializer_write_for_slot(instruction, slot: int) -> bool:
1352
+ """Return whether an instruction can extend one freshly allocated table."""
1353
+
1354
+ if instruction.opcode == "EXTRAARG":
1355
+ return True
1356
+ if instruction.opcode not in {"SETFIELD", "SETI", "SETTABLE", "SETLIST"}:
1357
+ return False
1358
+ return bool(instruction.operands) and int(instruction.operands[0]) == slot
1359
+
1360
+
1361
+ def _table_initializer_local_for_slot_at_pc(
1362
+ listing: LuaFunctionListing,
1363
+ slot: int,
1364
+ pc: int,
1365
+ ):
1366
+ """Resolve a table register while its debug-local range has not opened."""
1367
+
1368
+ current = _instruction_at_pc(listing, pc)
1369
+ if current is None or not _is_table_initializer_write_for_slot(current, slot):
1370
+ return None
1371
+ for prior in reversed(listing.instructions):
1372
+ if prior.pc >= pc:
1373
+ continue
1374
+ if prior.opcode != "NEWTABLE" or not prior.operands or int(prior.operands[0]) != slot:
1375
+ continue
1376
+ local = _local_starting_after_table_initializer(listing, slot, prior.pc)
1377
+ if local is not None and prior.pc < pc < local.start_pc:
1378
+ return local
1379
+ return None
1380
+ return None
1381
+
1382
+
1383
+ def _instruction_writes_register(instruction, register: int) -> bool:
1384
+ if not instruction.operands or not instruction.operands[0].lstrip("-").isdigit():
1385
+ return False
1386
+ if instruction.opcode in {"CALL", "TAILCALL"}:
1387
+ # A call can write its base and, when it returns multiple values,
1388
+ # following registers as well. Treat an overlapping base as a
1389
+ # clobber; this is conservative and avoids renaming a reused slot.
1390
+ return int(instruction.operands[0]) <= register
1391
+ if instruction.opcode in LUA_REGISTER_DEST_OPS:
1392
+ return int(instruction.operands[0]) == register
1393
+ return instruction.opcode == "TESTSET" and int(instruction.operands[0]) == register
1394
+
1395
+
1396
+ def _local_starting_after_setlist_read(
1397
+ listing: LuaFunctionListing,
1398
+ slot: int,
1399
+ pc: int,
1400
+ ):
1401
+ instruction = _instruction_at_pc(listing, pc)
1402
+ if instruction is None or instruction.opcode != "SETLIST" or not instruction.operands:
1403
+ return None
1404
+ if int(instruction.operands[0]) != slot:
1405
+ return None
1406
+ return _nearest_user_local_start(listing, slot, pc + 1, pc + 1)
1407
+
1408
+
1409
+ def _nearest_user_local_start(
1410
+ listing: LuaFunctionListing,
1411
+ slot: int,
1412
+ start_pc: int,
1413
+ end_pc: int,
1414
+ ):
1415
+ candidates = [
1416
+ local
1417
+ for local in listing.locals
1418
+ if local.slot == slot and start_pc <= local.start_pc <= end_pc and _looks_user_named(local.name)
1419
+ ]
1420
+ if not candidates:
1421
+ return None
1422
+ return min(candidates, key=lambda local: local.start_pc)
1423
+
1424
+
1425
+ def _instruction_at_pc(listing: LuaFunctionListing, pc: int):
1426
+ return next((instruction for instruction in listing.instructions if instruction.pc == pc), None)
1427
+
1428
+
1429
+ def _next_instruction_reads_register(
1430
+ listing: LuaFunctionListing,
1431
+ pc: int,
1432
+ register: int,
1433
+ ) -> bool:
1434
+ next_instruction = next((instruction for instruction in listing.instructions if instruction.pc == pc + 1), None)
1435
+ if next_instruction is None:
1436
+ return False
1437
+ return register in _read_registers_for_instruction(next_instruction)
1438
+
1439
+
1440
+ def _read_registers_for_instruction(instruction) -> set[int]:
1441
+ operands = instruction.operands
1442
+ if not operands:
1443
+ return set()
1444
+ opcode = instruction.opcode
1445
+ result: set[int] = set()
1446
+ if opcode in {"MOVE", "UNM", "BNOT", "NOT", "LEN"} and len(operands) >= 2:
1447
+ result.add(int(operands[1]))
1448
+ elif opcode in {"GETTABLE", "GETFIELD", "GETI"} and len(operands) >= 2:
1449
+ result.add(int(operands[1]))
1450
+ if opcode == "GETTABLE" and len(operands) >= 3 and not operands[2].endswith("k"):
1451
+ result.add(int(operands[2]))
1452
+ elif opcode in {"SETTABLE", "SETFIELD", "SETI"}:
1453
+ result.add(int(operands[0]))
1454
+ if opcode == "SETTABLE" and len(operands) >= 2 and not operands[1].endswith("k"):
1455
+ result.add(int(operands[1]))
1456
+ if len(operands) >= 3 and not operands[2].endswith("k"):
1457
+ result.add(int(operands[2]))
1458
+ elif opcode == "SELF" and len(operands) >= 2:
1459
+ result.add(int(operands[1]))
1460
+ elif opcode == "CONCAT":
1461
+ start = int(operands[1]) if len(operands) >= 3 else int(operands[0])
1462
+ end = int(operands[2]) if len(operands) >= 3 else int(operands[0]) + int(operands[1]) - 1
1463
+ result.update(range(start, end + 1))
1464
+ elif opcode in BINARY_OPS or opcode in {
1465
+ "BAND",
1466
+ "BOR",
1467
+ "BXOR",
1468
+ "SHL",
1469
+ "SHR",
1470
+ "POW",
1471
+ }:
1472
+ if len(operands) >= 3:
1473
+ result.update((int(operands[1]), int(operands[2])))
1474
+ elif opcode in IMMEDIATE_BINARY_OPS or opcode in {"SHRI", "SHLI"}:
1475
+ if len(operands) >= 2:
1476
+ result.add(int(operands[1]))
1477
+ elif opcode.endswith("K") and opcode not in {"LOADK", "LOADKX"}:
1478
+ if len(operands) >= 2:
1479
+ result.add(int(operands[1]))
1480
+ elif opcode in {"CALL", "TAILCALL"} and len(operands) >= 2:
1481
+ base = int(operands[0])
1482
+ arg_count = int(operands[1]) - 1
1483
+ result.add(base)
1484
+ if arg_count >= 0:
1485
+ result.update(range(base + 1, base + 1 + arg_count))
1486
+ elif opcode in COMPARISON_OPS and len(operands) >= 2:
1487
+ result.update((int(operands[0]), int(operands[1])))
1488
+ elif opcode in set(IMMEDIATE_COMPARISON_OPS) | {"TEST"} and operands:
1489
+ result.add(int(operands[0]))
1490
+ elif opcode == "RETURN" and len(operands) >= 2:
1491
+ start = int(operands[0])
1492
+ count = int(operands[1]) - 1
1493
+ if count >= 0:
1494
+ result.update(range(start, start + count))
1495
+ elif opcode in {"RETURN1", "RETURNI", "RETURNK"}:
1496
+ result.add(int(operands[0]))
1497
+ return result
1498
+
1499
+
1500
+ def _looks_user_named(name: str) -> bool:
1501
+ return not (name.startswith("(") and name.endswith(")"))
1502
+
1503
+
1504
+ def _is_root_chunk(listing: LuaFunctionListing) -> bool:
1505
+ return listing.inferred_name == "<chunk>" and listing.line_start == 0
1506
+
1507
+
1508
+ def _constant_operand_expr(
1509
+ constants: dict[int, object],
1510
+ operand: str,
1511
+ source: SourceRef,
1512
+ ) -> Expr:
1513
+ constant_index = int(operand.removesuffix("k"))
1514
+ return Const(value=constants.get(constant_index), source=source)
1515
+
1516
+
1517
+ def _operand_expr(
1518
+ listing: LuaFunctionListing,
1519
+ constants: dict[int, object],
1520
+ operand: str,
1521
+ pc: int,
1522
+ source: SourceRef,
1523
+ ) -> Expr:
1524
+ if operand.endswith("k"):
1525
+ return _constant_operand_expr(constants, operand, source)
1526
+ return Var(name=_read_register_name(listing, int(operand), pc), source=source)
1527
+
1528
+
1529
+ def _lua_call_argument_expr(
1530
+ listing: LuaFunctionListing,
1531
+ register: int,
1532
+ pc: int,
1533
+ source: SourceRef,
1534
+ ) -> Expr:
1535
+ register_name = _read_register_name(listing, register, pc)
1536
+ return Var(name=register_name, source=source)
1537
+
1538
+
1539
+ def _lua_open_call_arg_count(listing: LuaFunctionListing, instruction) -> int:
1540
+ base = int(instruction.operands[0])
1541
+ previous = next(
1542
+ (prior for prior in reversed(listing.instructions) if prior.pc < instruction.pc),
1543
+ None,
1544
+ )
1545
+ if (
1546
+ previous is not None
1547
+ and previous.opcode == "CALL"
1548
+ and len(previous.operands) >= 3
1549
+ and int(previous.operands[0]) == base + 1
1550
+ and int(previous.operands[2]) == 0
1551
+ ):
1552
+ return 1
1553
+ highest = base
1554
+ for prior in listing.instructions:
1555
+ if prior.pc >= instruction.pc:
1556
+ break
1557
+ if not prior.operands:
1558
+ continue
1559
+ if not prior.operands[0].lstrip("-").isdigit():
1560
+ continue
1561
+ if prior.opcode not in LUA_REGISTER_DEST_OPS:
1562
+ continue
1563
+ dest = int(prior.operands[0])
1564
+ if dest > highest:
1565
+ highest = dest
1566
+ return max(0, highest - base)
1567
+
1568
+
1569
+ LUA_REGISTER_DEST_OPS = frozenset(
1570
+ {
1571
+ "ADDI",
1572
+ "ADDK",
1573
+ "ADD",
1574
+ "BAND",
1575
+ "BANDK",
1576
+ "BOR",
1577
+ "BORK",
1578
+ "BXOR",
1579
+ "BXORK",
1580
+ "CALL",
1581
+ "CONCAT",
1582
+ "CLOSURE",
1583
+ "DIV",
1584
+ "DIVK",
1585
+ "EQ",
1586
+ "EQI",
1587
+ "EQK",
1588
+ "GETFIELD",
1589
+ "GETI",
1590
+ "GETTABLE",
1591
+ "GETTABUP",
1592
+ "GETUPVAL",
1593
+ "IDIV",
1594
+ "IDIVK",
1595
+ "LOADFALSE",
1596
+ "LOADF",
1597
+ "LOADI",
1598
+ "LOADK",
1599
+ "LOADKX",
1600
+ "LOADNIL",
1601
+ "LOADTRUE",
1602
+ "MOD",
1603
+ "MODK",
1604
+ "MUL",
1605
+ "MULK",
1606
+ "NEWTABLE",
1607
+ "POW",
1608
+ "POWK",
1609
+ "SELF",
1610
+ "SHL",
1611
+ "SHLI",
1612
+ "SHR",
1613
+ "SHRI",
1614
+ "SUB",
1615
+ "SUBK",
1616
+ "VARARG",
1617
+ }
1618
+ )
1619
+
1620
+
1621
+ def _lua_raw_instruction_window(
1622
+ instructions: tuple[object, ...],
1623
+ index: int,
1624
+ radius: int = 3,
1625
+ ) -> tuple[str, ...]:
1626
+ start = max(0, index - radius)
1627
+ end = min(len(instructions), index + radius + 1)
1628
+ return tuple(
1629
+ f"{instruction.pc}: {instruction.opcode} {' '.join(instruction.operands)}".strip()
1630
+ for instruction in instructions[start:end]
1631
+ )