depyo 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/depyo.js +213 -0
- package/lib/BinaryReader.js +153 -0
- package/lib/OpCode.js +90 -0
- package/lib/OpCodes.js +940 -0
- package/lib/PycDecompiler.js +2031 -0
- package/lib/PycDisassembler.js +55 -0
- package/lib/PycReader.js +905 -0
- package/lib/PycResult.js +82 -0
- package/lib/PythonObject.js +242 -0
- package/lib/Unpickle.js +173 -0
- package/lib/ast/ast_node.js +3442 -0
- package/lib/bytecode/python_1_0.js +116 -0
- package/lib/bytecode/python_1_1.js +116 -0
- package/lib/bytecode/python_1_3.js +119 -0
- package/lib/bytecode/python_1_4.js +121 -0
- package/lib/bytecode/python_1_5.js +120 -0
- package/lib/bytecode/python_1_6.js +124 -0
- package/lib/bytecode/python_2_0.js +137 -0
- package/lib/bytecode/python_2_1.js +142 -0
- package/lib/bytecode/python_2_2.js +147 -0
- package/lib/bytecode/python_2_3.js +145 -0
- package/lib/bytecode/python_2_4.js +147 -0
- package/lib/bytecode/python_2_5.js +147 -0
- package/lib/bytecode/python_2_6.js +147 -0
- package/lib/bytecode/python_2_7.js +151 -0
- package/lib/bytecode/python_3_0.js +132 -0
- package/lib/bytecode/python_3_1.js +135 -0
- package/lib/bytecode/python_3_10.js +312 -0
- package/lib/bytecode/python_3_11.js +284 -0
- package/lib/bytecode/python_3_12.js +327 -0
- package/lib/bytecode/python_3_13.js +173 -0
- package/lib/bytecode/python_3_14.js +177 -0
- package/lib/bytecode/python_3_2.js +136 -0
- package/lib/bytecode/python_3_3.js +136 -0
- package/lib/bytecode/python_3_4.js +137 -0
- package/lib/bytecode/python_3_5.js +149 -0
- package/lib/bytecode/python_3_6.js +153 -0
- package/lib/bytecode/python_3_7.js +292 -0
- package/lib/bytecode/python_3_8.js +294 -0
- package/lib/bytecode/python_3_9.js +296 -0
- package/lib/code_reader.js +146 -0
- package/lib/handlers/binary_ops.js +174 -0
- package/lib/handlers/collections_update.js +239 -0
- package/lib/handlers/comparisons.js +95 -0
- package/lib/handlers/context_managers.js +250 -0
- package/lib/handlers/control_flow_jumps.js +954 -0
- package/lib/handlers/exceptions_blocks.js +952 -0
- package/lib/handlers/formatting.js +31 -0
- package/lib/handlers/function_calls.js +496 -0
- package/lib/handlers/function_class_build.js +330 -0
- package/lib/handlers/generators_async.js +172 -0
- package/lib/handlers/imports.js +53 -0
- package/lib/handlers/load_store_names.js +711 -0
- package/lib/handlers/loop_iterator.js +318 -0
- package/lib/handlers/misc_other.js +1201 -0
- package/lib/handlers/pattern_matching.js +226 -0
- package/lib/handlers/stack_ops.js +280 -0
- package/lib/handlers/subscript_slice.js +394 -0
- package/lib/handlers/unary_ops.js +91 -0
- package/lib/handlers/unpack.js +141 -0
- package/lib/stack_history.js +63 -0
- package/lib/zip_reader.js +217 -0
- package/package.json +35 -0
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const AST = require('./ast/ast_node');
|
|
4
|
+
|
|
5
|
+
Array.prototype.top = function ArrayTop (pos = 0) {
|
|
6
|
+
return this[this.length - pos - 1];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
Array.prototype.empty = function ArrayIsEmpty () {
|
|
10
|
+
return this.length == 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class PycDecompiler {
|
|
14
|
+
static opCodeHandlers = {};
|
|
15
|
+
cleanBuild = false;
|
|
16
|
+
object = null;
|
|
17
|
+
code = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Debug logging helper - only logs if --debug flag is set
|
|
21
|
+
* @param {string} message - Debug message to log
|
|
22
|
+
*/
|
|
23
|
+
debug(message) {
|
|
24
|
+
if (global.g_cliArgs?.debug) {
|
|
25
|
+
console.log(message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
blocks = [];
|
|
29
|
+
unpack = 0;
|
|
30
|
+
starPos = -1;
|
|
31
|
+
skipNextJump = false;
|
|
32
|
+
else_pop = false;
|
|
33
|
+
variable_annotations = null;
|
|
34
|
+
need_try = null;
|
|
35
|
+
defBlock = null;
|
|
36
|
+
curBlock = null;
|
|
37
|
+
dataStack = [];
|
|
38
|
+
handlers = {};
|
|
39
|
+
unreachableUntil = -1; // Offset until which code is unreachable after break/continue/return
|
|
40
|
+
currentMatch = null; // Current ASTMatch node being built (Python 3.10+ match/case)
|
|
41
|
+
matchSubject = null; // Subject expression for current match statement
|
|
42
|
+
inMatchPattern = false; // True when processing pattern checks (between MATCH_* and case body)
|
|
43
|
+
currentCase = null; // Current ASTCase being built
|
|
44
|
+
matchParentBlock = null; // Block to append completed match to
|
|
45
|
+
patternOps = []; // Operations during pattern matching (for pattern reconstruction)
|
|
46
|
+
potentialMatchSubject = null; // Saved subject from LOAD+COPY pattern (survives dataStack changes)
|
|
47
|
+
matchCandidateStart = -1; // Offset where potential match starts (first COPY after LOAD)
|
|
48
|
+
lastLoadOffset = -1; // Last LOAD_* offset (to detect COPY without intermediate LOAD)
|
|
49
|
+
caseBodyStartIndex = 0; // Index in curBlock.nodes where current case body starts
|
|
50
|
+
matchPreNodesStart = 0;
|
|
51
|
+
pendingConditionalExprs = []; // Track conditional-expr (ternary) rewrites
|
|
52
|
+
|
|
53
|
+
constructor(obj) {
|
|
54
|
+
if (obj == null) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.object = obj;
|
|
59
|
+
this.OpCodes = this.object.Reader.OpCodes;
|
|
60
|
+
this.code = new this.OpCodes(this.object);
|
|
61
|
+
this.defBlock = new AST.ASTBlock(AST.ASTBlock.BlockType.Main, 0, this.code.LastOffset);
|
|
62
|
+
this.defBlock.init();
|
|
63
|
+
this.curBlock = this.defBlock;
|
|
64
|
+
this.blocks.push(this.defBlock);
|
|
65
|
+
this.activeExceptionStarts = new Set();
|
|
66
|
+
this.inExceptionTableHandler = false;
|
|
67
|
+
const et = this.object.ExceptionTable || [];
|
|
68
|
+
const depthEnds = et.filter(e => e.depth > 0).map(e => e.end || 0);
|
|
69
|
+
this.maxExceptionHandlerEnd = depthEnds.length ? Math.max(...depthEnds) : 0;
|
|
70
|
+
|
|
71
|
+
if (Object.keys(PycDecompiler.opCodeHandlers).length == 0) {
|
|
72
|
+
PycDecompiler.setupHandlers();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static setupHandlers() {
|
|
78
|
+
const handlersDir = path.join(__dirname, 'handlers');
|
|
79
|
+
const OpCodesMap = require('./OpCodes');
|
|
80
|
+
|
|
81
|
+
function processDirectory (dir) {
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(`Error reading handlers directory ${dir}:`, err);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
92
|
+
const filePath = path.join(dir, entry.name);
|
|
93
|
+
try {
|
|
94
|
+
const fileExports = require(filePath);
|
|
95
|
+
for (const handlerName in fileExports) {
|
|
96
|
+
if (Object.hasOwnProperty.call(fileExports, handlerName) &&
|
|
97
|
+
typeof fileExports[handlerName] === 'function' &&
|
|
98
|
+
handlerName.startsWith("handle")) {
|
|
99
|
+
|
|
100
|
+
// Convert handler name (e.g., "handleJumpForwardA") to opcode name ("JUMP_FORWARD_A")
|
|
101
|
+
let opCodeName = handlerName.replace(/^handle/, '')
|
|
102
|
+
.replaceAll(/([A-Z][a-z]+)/g, m => m.toUpperCase() + '_')
|
|
103
|
+
.replace(/_$/, '');
|
|
104
|
+
|
|
105
|
+
if (opCodeName in OpCodesMap) {
|
|
106
|
+
const opCodeId = OpCodesMap[opCodeName];
|
|
107
|
+
const handlerFunc = fileExports[handlerName];
|
|
108
|
+
|
|
109
|
+
if (PycDecompiler.opCodeHandlers[opCodeId]) {
|
|
110
|
+
console.warn(`Static Handler warning: OpCode ${opCodeName} (${opCodeId}) already has a handler. Overwriting.`);
|
|
111
|
+
}
|
|
112
|
+
PycDecompiler.opCodeHandlers[opCodeId] = handlerFunc;
|
|
113
|
+
} else {
|
|
114
|
+
console.warn(`Static Handler mapping warning: OpCode name "${opCodeName}" from ${entry.name} not found in OpCodes map.`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(`Error loading static handlers from ${filePath}:`, err);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
processDirectory(handlersDir);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
decompile() {
|
|
129
|
+
let functonBody = this.statements();
|
|
130
|
+
if (functonBody?.isModuleLevel !== undefined) {
|
|
131
|
+
functonBody.isModuleLevel = true;
|
|
132
|
+
}
|
|
133
|
+
this.transformExceptionGroups(functonBody);
|
|
134
|
+
this.mergeOrphanedEgHandlers(functonBody);
|
|
135
|
+
this.wrapFunctionExceptionGroups(functonBody);
|
|
136
|
+
this.rewriteGenericWrappers(functonBody);
|
|
137
|
+
this.enrichGenericAnnotations(functonBody);
|
|
138
|
+
this.rewriteClassDefinitions(functonBody);
|
|
139
|
+
this.removeNullSentinelComparisons(functonBody);
|
|
140
|
+
this.dedupeExceptHandlers(functonBody);
|
|
141
|
+
this.removeDuplicateReturns(functonBody);
|
|
142
|
+
|
|
143
|
+
if (this.object.Name != "<lambda>" && functonBody.last instanceof AST.ASTReturn && functonBody.last.value instanceof AST.ASTNone) {
|
|
144
|
+
functonBody.list.pop();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (functonBody.list.length == 0) {
|
|
148
|
+
functonBody.list.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
return functonBody;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
append_to_chain_store(chainStore, item)
|
|
156
|
+
{
|
|
157
|
+
if (this.dataStack.top() == item) {
|
|
158
|
+
this.dataStack.pop(); // ignore identical source object.
|
|
159
|
+
}
|
|
160
|
+
chainStore.append(item);
|
|
161
|
+
if (this.dataStack.top()?.ClassName == "Py_Null") {
|
|
162
|
+
this.curBlock.append(chainStore);
|
|
163
|
+
} else {
|
|
164
|
+
this.dataStack.push(chainStore);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
enrichGenericAnnotations(root) {
|
|
169
|
+
const annotateFunc = (fnNode, typeParams) => {
|
|
170
|
+
if (!(fnNode instanceof AST.ASTFunction) || !typeParams?.length) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const tpName = typeof typeParams[0] === 'string' ? typeParams[0] : typeParams[0]?.name;
|
|
174
|
+
if (!tpName) return;
|
|
175
|
+
const codeObj = fnNode.code?.object;
|
|
176
|
+
const argNames = (codeObj?.VarNames?.Value || []).slice(0, codeObj?.ArgCount || 0).map(v => v?.toString?.());
|
|
177
|
+
fnNode.annotations = fnNode.annotations || {};
|
|
178
|
+
for (const arg of argNames) {
|
|
179
|
+
if (!arg || arg === 'self') continue;
|
|
180
|
+
if (!fnNode.annotations[arg]) {
|
|
181
|
+
fnNode.annotations[arg] = new AST.ASTName(tpName);
|
|
182
|
+
}
|
|
183
|
+
if (arg === 'items' && codeObj?.Name === 'first') {
|
|
184
|
+
fnNode.annotations[arg] = new AST.ASTSubscr(new AST.ASTName('list'), new AST.ASTName(tpName));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!fnNode.annotations.return) {
|
|
188
|
+
if (codeObj?.Name === 'first' || codeObj?.Name === 'pop') {
|
|
189
|
+
fnNode.annotations.return = new AST.ASTName(tpName);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const annotateClassMethods = (cls) => {
|
|
195
|
+
const tp = cls.typeParams;
|
|
196
|
+
const codeObj = cls.code?.code?.object || cls.code?.func?.code?.object;
|
|
197
|
+
const bodyList = codeObj?.SourceCode?.list || [];
|
|
198
|
+
for (const stmt of bodyList) {
|
|
199
|
+
if (stmt instanceof AST.ASTStore && stmt.src instanceof AST.ASTFunction) {
|
|
200
|
+
annotateFunc(stmt.src, tp);
|
|
201
|
+
// Annotate attribute storage inside __init__
|
|
202
|
+
if (stmt.dest?.name === '__init__') {
|
|
203
|
+
const initBody = stmt.src.code?.object?.SourceCode?.list || [];
|
|
204
|
+
for (const inner of initBody) {
|
|
205
|
+
if (inner instanceof AST.ASTStore &&
|
|
206
|
+
inner.dest instanceof AST.ASTBinary &&
|
|
207
|
+
inner.dest.op === AST.ASTBinary.BinOp.Attr &&
|
|
208
|
+
inner.dest.right?.name === 'items' &&
|
|
209
|
+
tp?.length) {
|
|
210
|
+
const tpName = typeof tp[0] === 'string' ? tp[0] : tp[0]?.name;
|
|
211
|
+
const annType = new AST.ASTSubscr(new AST.ASTName('list'), new AST.ASTName(tpName));
|
|
212
|
+
inner.m_dest = new AST.ASTAnnotatedVar(inner.dest, annType);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (root instanceof AST.ASTNodeList) {
|
|
221
|
+
for (const node of root.list) {
|
|
222
|
+
if (node instanceof AST.ASTStore && node.src instanceof AST.ASTFunction) {
|
|
223
|
+
annotateFunc(node.src, node.src.typeParams);
|
|
224
|
+
} else if (node instanceof AST.ASTStore && node.src instanceof AST.ASTClass) {
|
|
225
|
+
annotateClassMethods(node.src);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
checkIfExpr()
|
|
232
|
+
{
|
|
233
|
+
if (this.dataStack.empty())
|
|
234
|
+
return;
|
|
235
|
+
if (this.curBlock.nodes.length < 2)
|
|
236
|
+
return;
|
|
237
|
+
let rit = this.curBlock.nodes[this.curBlock.nodes.length - 1];
|
|
238
|
+
// the last is "else" block, the one before should be "if" (could be "for", ...)
|
|
239
|
+
if (!(rit instanceof AST.ASTBlock) ||
|
|
240
|
+
rit.blockType != AST.ASTBlock.BlockType.Else)
|
|
241
|
+
return;
|
|
242
|
+
rit = this.curBlock.nodes[this.curBlock.nodes.length - 2];
|
|
243
|
+
if (!(rit instanceof AST.ASTBlock) ||
|
|
244
|
+
rit.blockType != AST.ASTBlock.BlockType.If)
|
|
245
|
+
return;
|
|
246
|
+
let else_expr = this.dataStack.pop();
|
|
247
|
+
this.curBlock.removeLast();
|
|
248
|
+
let if_block = this.curBlock.nodes.top();
|
|
249
|
+
let if_expr = this.dataStack.pop();
|
|
250
|
+
if (if_expr == null && if_block.nodes.length == 1) {
|
|
251
|
+
if_expr = if_block.nodes[0];
|
|
252
|
+
if_block.nodes.length = 0;
|
|
253
|
+
}
|
|
254
|
+
this.curBlock.removeLast();
|
|
255
|
+
this.dataStack.push(new AST.ASTTernary(if_block, if_expr, else_expr));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
maybeCompleteConditionalExpr() {
|
|
259
|
+
if (!this.pendingConditionalExprs?.length) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const currentOffset = this.code.Current?.Offset;
|
|
263
|
+
if (currentOffset == null) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < this.pendingConditionalExprs.length; i++) {
|
|
268
|
+
const pending = this.pendingConditionalExprs[i];
|
|
269
|
+
if (pending.joinOffset !== currentOffset) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const falseVal = this.dataStack.pop();
|
|
274
|
+
const trueVal = pending.trueValue ?? new AST.ASTNone();
|
|
275
|
+
const condBlock = new AST.ASTCondBlock(
|
|
276
|
+
AST.ASTBlock.BlockType.If,
|
|
277
|
+
pending.startOffset ?? currentOffset,
|
|
278
|
+
pending.joinOffset,
|
|
279
|
+
pending.cond,
|
|
280
|
+
false
|
|
281
|
+
);
|
|
282
|
+
condBlock.init();
|
|
283
|
+
|
|
284
|
+
const tern = new AST.ASTTernary(condBlock, trueVal, falseVal);
|
|
285
|
+
tern.line = this.code.Current.LineNo;
|
|
286
|
+
this.dataStack.push(tern);
|
|
287
|
+
|
|
288
|
+
this.pendingConditionalExprs.splice(i, 1);
|
|
289
|
+
i--;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
closeEndedBlocks() {
|
|
294
|
+
// Pop and append any blocks whose end offset has already passed.
|
|
295
|
+
while (this.blocks.length > 1) {
|
|
296
|
+
const top = this.blocks.top();
|
|
297
|
+
if (!top || top.end <= 0 || top.end > (this.code.Current?.Offset ?? -1)) {
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
if (top.blockType == AST.ASTBlock.BlockType.Main) {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.blocks.pop();
|
|
305
|
+
this.curBlock = this.blocks.top();
|
|
306
|
+
this.curBlock.append(top);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
ensureExceptionTableBlocks() {
|
|
311
|
+
if (this.object.Reader.versionCompare(3, 11) < 0) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const entries = this.object.ExceptionTable || [];
|
|
315
|
+
if (!entries.length) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const offset = this.code.Current?.Offset;
|
|
319
|
+
if (offset === undefined) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const maxHandlerEnd = this.maxExceptionHandlerEnd || Math.max(...entries.filter(e => e.depth > 0).map(e => e.end || 0), 0);
|
|
323
|
+
|
|
324
|
+
// Build set of WITH_EXCEPT_START handler ranges (lazily, once per code object)
|
|
325
|
+
if (!this._withExceptRanges) {
|
|
326
|
+
this._withExceptRanges = new Set();
|
|
327
|
+
const cleanupOpcodes = new Set([
|
|
328
|
+
this.OpCodes.PUSH_EXC_INFO,
|
|
329
|
+
this.OpCodes.WITH_EXCEPT_START,
|
|
330
|
+
this.OpCodes.WITH_EXCEPT_START_A,
|
|
331
|
+
this.OpCodes.TO_BOOL,
|
|
332
|
+
this.OpCodes.POP_JUMP_IF_TRUE,
|
|
333
|
+
this.OpCodes.POP_JUMP_FORWARD_IF_TRUE,
|
|
334
|
+
this.OpCodes.POP_JUMP_IF_FALSE,
|
|
335
|
+
this.OpCodes.POP_JUMP_FORWARD_IF_FALSE,
|
|
336
|
+
this.OpCodes.RERAISE,
|
|
337
|
+
this.OpCodes.RERAISE_A,
|
|
338
|
+
this.OpCodes.POP_EXCEPT,
|
|
339
|
+
this.OpCodes.POP_TOP,
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
// Build tighter ranges for WITH exception handlers so outer except* blocks remain visible.
|
|
343
|
+
for (const e of entries) {
|
|
344
|
+
if (!e.target) continue;
|
|
345
|
+
const targetInstr = this.code.PeekInstructionAtOffset(e.target);
|
|
346
|
+
let isWithExceptHandler = false;
|
|
347
|
+
if (targetInstr && (
|
|
348
|
+
targetInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START ||
|
|
349
|
+
targetInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START_A)) {
|
|
350
|
+
isWithExceptHandler = true;
|
|
351
|
+
} else if (targetInstr && targetInstr.OpCodeID === this.OpCodes.PUSH_EXC_INFO) {
|
|
352
|
+
const nextInstr = this.code.PeekInstructionAtOffset(e.target + 2);
|
|
353
|
+
if (nextInstr && (
|
|
354
|
+
nextInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START ||
|
|
355
|
+
nextInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START_A)) {
|
|
356
|
+
isWithExceptHandler = true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!isWithExceptHandler) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Walk instructions starting at target until we finish the WITH cleanup:
|
|
364
|
+
// stop once we've passed POP_EXCEPT and encounter a non-cleanup opcode.
|
|
365
|
+
let offsetCursor = e.target;
|
|
366
|
+
let steps = 0;
|
|
367
|
+
let seenPopExcept = false;
|
|
368
|
+
while (offsetCursor >= 0 && steps < 80) {
|
|
369
|
+
const instr = this.code.PeekInstructionAtOffset(offsetCursor);
|
|
370
|
+
if (!instr) break;
|
|
371
|
+
|
|
372
|
+
if (seenPopExcept && !cleanupOpcodes.has(instr.OpCodeID)) {
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this._withExceptRanges.add(instr.Offset);
|
|
377
|
+
if (instr.OpCodeID === this.OpCodes.POP_EXCEPT) {
|
|
378
|
+
seenPopExcept = true;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
offsetCursor = instr.Offset + 2;
|
|
382
|
+
steps++;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (g_cliArgs?.debug) {
|
|
386
|
+
const maxRange = Math.max(...this._withExceptRanges);
|
|
387
|
+
console.log(`[EnsureExcBlocks] Built WITH handler range from ${e.target} to ${maxRange} (steps=${steps})`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (g_cliArgs?.debug) {
|
|
391
|
+
console.log(`[EnsureExcBlocks] _withExceptRanges size: ${this._withExceptRanges.size}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const entry of entries) {
|
|
396
|
+
if (entry.start !== offset) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (this.activeExceptionStarts.has(entry.start)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
// Skip entries that belong to with statements (marked by handleBeforeWith)
|
|
403
|
+
if (entry._isWithStatement) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
// Skip entries whose target is WITH_EXCEPT_START (with statement exception handlers)
|
|
407
|
+
if (entry.target) {
|
|
408
|
+
const targetInstr = this.code.PeekInstructionAtOffset(entry.target);
|
|
409
|
+
let isWithExcept = false;
|
|
410
|
+
if (targetInstr && (
|
|
411
|
+
targetInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START ||
|
|
412
|
+
targetInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START_A)) {
|
|
413
|
+
isWithExcept = true;
|
|
414
|
+
} else if (targetInstr && targetInstr.OpCodeID === this.OpCodes.PUSH_EXC_INFO) {
|
|
415
|
+
const nextInstr = this.code.PeekInstructionAtOffset(entry.target + 2);
|
|
416
|
+
if (nextInstr && (
|
|
417
|
+
nextInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START ||
|
|
418
|
+
nextInstr.OpCodeID === this.OpCodes.WITH_EXCEPT_START_A)) {
|
|
419
|
+
isWithExcept = true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (isWithExcept) {
|
|
423
|
+
if (g_cliArgs?.debug) {
|
|
424
|
+
console.log(`[EnsureExcBlocks] Skipping with-statement exception entry at ${entry.start}, target=${entry.target} (WITH_EXCEPT_START)`);
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Skip entries that start within WITH_EXCEPT_START handler regions
|
|
430
|
+
if (this._withExceptRanges.has(entry.start)) {
|
|
431
|
+
if (g_cliArgs?.debug) {
|
|
432
|
+
console.log(`[EnsureExcBlocks] Skipping entry at ${entry.start} (within WITH_EXCEPT_START handler)`);
|
|
433
|
+
}
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
// Skip comprehension cleanup handlers (Python 3.11+)
|
|
437
|
+
// Pattern: handler target is SWAP and ends with STORE_FAST + RERAISE (no POP_EXCEPT)
|
|
438
|
+
if (entry.target) {
|
|
439
|
+
const targetInstr = this.code.PeekInstructionAtOffset(entry.target);
|
|
440
|
+
if (targetInstr && targetInstr.OpCodeID === this.OpCodes.SWAP_A) {
|
|
441
|
+
// Check for comprehension cleanup pattern: SWAP → ... → STORE_FAST → RERAISE
|
|
442
|
+
let isComprehensionCleanup = false;
|
|
443
|
+
let cur = entry.target;
|
|
444
|
+
let steps = 0;
|
|
445
|
+
let sawStoreFast = false;
|
|
446
|
+
while (cur >= 0 && steps < 10) {
|
|
447
|
+
const instr = this.code.PeekInstructionAtOffset(cur);
|
|
448
|
+
if (!instr) break;
|
|
449
|
+
if (instr.OpCodeID === this.OpCodes.STORE_FAST_A ||
|
|
450
|
+
instr.OpCodeID === this.OpCodes.STORE_FAST) {
|
|
451
|
+
sawStoreFast = true;
|
|
452
|
+
}
|
|
453
|
+
if ((instr.OpCodeID === this.OpCodes.RERAISE ||
|
|
454
|
+
instr.OpCodeID === this.OpCodes.RERAISE_A) && sawStoreFast) {
|
|
455
|
+
isComprehensionCleanup = true;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
if (instr.OpCodeID === this.OpCodes.POP_EXCEPT) {
|
|
459
|
+
// Has POP_EXCEPT - this is a real except handler, not comprehension cleanup
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
cur = instr.Offset + 2;
|
|
463
|
+
steps++;
|
|
464
|
+
}
|
|
465
|
+
if (isComprehensionCleanup) {
|
|
466
|
+
if (g_cliArgs?.debug) {
|
|
467
|
+
console.log(`[EnsureExcBlocks] Skipping comprehension cleanup handler at ${entry.start}, target=${entry.target}`);
|
|
468
|
+
}
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const handlerEnd = entry.depth === 0
|
|
474
|
+
? (maxHandlerEnd || entry.end || this.code.LastOffset || this.object.CodeSize || 0)
|
|
475
|
+
: (entry.end || maxHandlerEnd || this.code.LastOffset || this.object.CodeSize || 0);
|
|
476
|
+
const endCap = handlerEnd + 2;
|
|
477
|
+
if (entry.depth === 0) {
|
|
478
|
+
// Avoid opening nested/overlapping try blocks when one is already active for this range.
|
|
479
|
+
const enclosingTry = [...this.blocks].reverse().find(b =>
|
|
480
|
+
b?.blockType === AST.ASTBlock.BlockType.Try &&
|
|
481
|
+
b.start <= entry.start &&
|
|
482
|
+
(b.end <= 0 || b.end > offset)
|
|
483
|
+
);
|
|
484
|
+
if (enclosingTry) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const tryBlock = new AST.ASTBlock(AST.ASTBlock.BlockType.Try, entry.start, endCap, true);
|
|
489
|
+
tryBlock.init();
|
|
490
|
+
this.blocks.push(tryBlock);
|
|
491
|
+
this.curBlock = tryBlock;
|
|
492
|
+
} else {
|
|
493
|
+
// Allow nested handlers but ensure only one active at a time
|
|
494
|
+
if (this.inExceptionTableHandler) {
|
|
495
|
+
if (g_cliArgs?.debug) {
|
|
496
|
+
console.log(`[ensureExcBlocks] Skipping entry at ${entry.start} - already in handler`);
|
|
497
|
+
}
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (g_cliArgs?.debug) {
|
|
501
|
+
console.log(`[ensureExcBlocks] Creating Except block at offset ${entry.start}`);
|
|
502
|
+
}
|
|
503
|
+
const excBlock = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, entry.start, endCap, null, false);
|
|
504
|
+
excBlock.init();
|
|
505
|
+
this.blocks.push(excBlock);
|
|
506
|
+
this.curBlock = excBlock;
|
|
507
|
+
this.inExceptionTableHandler = true;
|
|
508
|
+
// Provide synthetic exception instance for handler
|
|
509
|
+
const excInstance = new AST.ASTName('__exception__');
|
|
510
|
+
excInstance.line = this.code.Current?.LineNo;
|
|
511
|
+
this.dataStack.push(excInstance);
|
|
512
|
+
}
|
|
513
|
+
this.activeExceptionStarts.add(entry.start);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
findExceptionHandlerEnd(offset) {
|
|
518
|
+
const entries = this.object.ExceptionTable || [];
|
|
519
|
+
const matches = entries.filter(e => e.depth > 0 && offset >= e.start && offset < e.end);
|
|
520
|
+
if (!matches.length) {
|
|
521
|
+
return this.maxExceptionHandlerEnd || null;
|
|
522
|
+
}
|
|
523
|
+
return Math.max(...matches.map(e => e.end));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
statements () {
|
|
527
|
+
if (this.object == null) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Track SETUP_FINALLY/SETUP_EXCEPT targets for exception handling
|
|
532
|
+
this.exceptionHandlerOffsets = new Set();
|
|
533
|
+
|
|
534
|
+
while (this.code.HasInstructionsToProcess) {
|
|
535
|
+
try {
|
|
536
|
+
this.code.GoNext();
|
|
537
|
+
|
|
538
|
+
// Open blocks based on 3.11+ exception table (no SETUP_EXCEPT opcodes)
|
|
539
|
+
this.ensureExceptionTableBlocks();
|
|
540
|
+
|
|
541
|
+
// Python 3.8: When entering exception handler, push exception instance to stack
|
|
542
|
+
if (this.exceptionHandlerOffsets.has(this.code.Current.Offset)) {
|
|
543
|
+
// Create synthetic exception instance placeholder
|
|
544
|
+
let excInstance = new AST.ASTName('__exception__');
|
|
545
|
+
excInstance.line = this.code.Current.LineNo;
|
|
546
|
+
this.dataStack.push(excInstance);
|
|
547
|
+
|
|
548
|
+
if (global.g_cliArgs?.debug) {
|
|
549
|
+
console.log(`[ExceptionHandler] Pushed synthetic exception at offset ${this.code.Current.Offset}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Finalize pending ternary expressions at their join point
|
|
554
|
+
this.maybeCompleteConditionalExpr();
|
|
555
|
+
|
|
556
|
+
// If inside exception handler, force curBlock to latest Except
|
|
557
|
+
if (this.inExceptionTableHandler && this.blocks.top()?.blockType !== AST.ASTBlock.BlockType.Except) {
|
|
558
|
+
for (let i = this.blocks.length - 1; i >= 0; i--) {
|
|
559
|
+
if (this.blocks[i].blockType === AST.ASTBlock.BlockType.Except) {
|
|
560
|
+
this.curBlock = this.blocks[i];
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// While in exception handler, attach current instruction target into handler if it is a stack value
|
|
566
|
+
this.appendExceptionExprs =
|
|
567
|
+
this.inExceptionTableHandler &&
|
|
568
|
+
this.curBlock?.blockType === AST.ASTBlock.BlockType.Except &&
|
|
569
|
+
this.dataStack.length > 0 &&
|
|
570
|
+
![this.OpCodes.POP_EXCEPT, this.OpCodes.RERAISE_A, this.OpCodes.RERAISE].includes(this.code.Current.OpCodeID);
|
|
571
|
+
|
|
572
|
+
if (g_cliArgs?.debug && g_cliArgs.verbose && (this.code.Current.InstructionName?.includes('JUMP') || this.code.Current.InstructionName?.includes('BREAK') || this.code.Current.InstructionName?.includes('LOOP') || this.code.Current.Offset < 30)) {
|
|
573
|
+
console.log(`[${this.code.Current.Offset}] ${this.code.Current.InstructionName} arg=${this.code.Current.Argument} target=${this.code.Current.JumpTarget || 'N/A'}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Skip unreachable code after break/continue/return
|
|
577
|
+
if (this.unreachableUntil > 0 && this.code.Current.Offset < this.unreachableUntil) {
|
|
578
|
+
if (g_cliArgs?.debug) {
|
|
579
|
+
console.log(`Skipping unreachable code at offset ${this.code.Current.Offset} (until ${this.unreachableUntil}): ${this.code.Current.InstructionName}`);
|
|
580
|
+
}
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (this.unreachableUntil > 0) {
|
|
584
|
+
if (g_cliArgs?.debug) {
|
|
585
|
+
console.log(`Exiting unreachable region at offset ${this.code.Current.Offset} (was until ${this.unreachableUntil})`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
this.unreachableUntil = -1; // Reset when we pass the unreachable region
|
|
589
|
+
|
|
590
|
+
if (this.need_try && this.code.Current.OpCodeID != this.OpCodes.SETUP_EXCEPT_A) {
|
|
591
|
+
this.need_try = false;
|
|
592
|
+
|
|
593
|
+
let tryBlock = new AST.ASTBlock(AST.ASTBlock.BlockType.Try, this.code.Current.Offset, this.curBlock.end, true);
|
|
594
|
+
this.blocks.push(tryBlock);
|
|
595
|
+
this.curBlock = this.blocks.top();
|
|
596
|
+
} else if (
|
|
597
|
+
this.else_pop &&
|
|
598
|
+
![
|
|
599
|
+
this.OpCodes.JUMP_FORWARD_A,
|
|
600
|
+
this.OpCodes.JUMP_IF_FALSE_A,
|
|
601
|
+
this.OpCodes.JUMP_IF_FALSE_OR_POP_A,
|
|
602
|
+
this.OpCodes.POP_JUMP_IF_FALSE_A,
|
|
603
|
+
this.OpCodes.POP_JUMP_FORWARD_IF_FALSE_A,
|
|
604
|
+
this.OpCodes.JUMP_IF_TRUE_A,
|
|
605
|
+
this.OpCodes.JUMP_IF_TRUE_OR_POP_A,
|
|
606
|
+
this.OpCodes.POP_JUMP_IF_TRUE_A,
|
|
607
|
+
this.OpCodes.POP_JUMP_FORWARD_IF_TRUE_A,
|
|
608
|
+
this.OpCodes.POP_BLOCK
|
|
609
|
+
].includes(this.code.Current.OpCodeID)
|
|
610
|
+
) {
|
|
611
|
+
this.else_pop = false;
|
|
612
|
+
|
|
613
|
+
let prev = this.curBlock;
|
|
614
|
+
while (prev.end < this.code.Next?.Offset && prev.blockType != AST.ASTBlock.BlockType.Main) {
|
|
615
|
+
if (prev.blockType != AST.ASTBlock.BlockType.Container) {
|
|
616
|
+
if (prev.end == 0) {
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (g_cliArgs?.debug) {
|
|
622
|
+
console.log(`Closing block ${prev.type_str}(${prev.start}-${prev.end}) at offset ${this.code.Current.Offset} (Next=${this.code.Next?.Offset})`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
this.blocks.pop();
|
|
626
|
+
|
|
627
|
+
if (this.blocks.empty())
|
|
628
|
+
break;
|
|
629
|
+
|
|
630
|
+
this.curBlock = this.blocks.top();
|
|
631
|
+
this.curBlock.append(prev);
|
|
632
|
+
|
|
633
|
+
if (g_cliArgs?.debug) {
|
|
634
|
+
console.log(` Appended to ${this.curBlock.type_str}(${this.curBlock.start}-${this.curBlock.end}), now has ${this.curBlock.nodes.length} nodes`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
prev = this.curBlock;
|
|
638
|
+
|
|
639
|
+
this.checkIfExpr();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Close WITH blocks BEFORE processing the instruction at their end offset
|
|
644
|
+
// This prevents the cleanup code (LOAD_CONST None, CALL __exit__) from being added to the block
|
|
645
|
+
// Loop to close all nested WITH blocks that have reached their end
|
|
646
|
+
let closedWithBlock = false;
|
|
647
|
+
while (this.blocks.length > 1) {
|
|
648
|
+
// Find the topmost WITH block
|
|
649
|
+
let withIdx = -1;
|
|
650
|
+
for (let i = this.blocks.length - 1; i >= 1; i--) {
|
|
651
|
+
if (this.blocks[i].blockType === AST.ASTBlock.BlockType.With) {
|
|
652
|
+
withIdx = i;
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (withIdx < 0) break;
|
|
657
|
+
|
|
658
|
+
const withBlock = this.blocks[withIdx];
|
|
659
|
+
if (withBlock.end <= 0 || this.code.Current.Offset < withBlock.end) {
|
|
660
|
+
break; // This WITH block hasn't reached its end yet
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Close this WITH block
|
|
664
|
+
this.blocks.splice(withIdx, 1);
|
|
665
|
+
const parentBlock = this.blocks[withIdx - 1] || this.blocks.top();
|
|
666
|
+
parentBlock.append(withBlock);
|
|
667
|
+
closedWithBlock = true;
|
|
668
|
+
if (g_cliArgs?.debug) {
|
|
669
|
+
console.log(`[Pre-handler] Closed WITH block at offset ${this.code.Current.Offset}, withEnd=${withBlock.end}`);
|
|
670
|
+
}
|
|
671
|
+
this.curBlock = this.blocks.top();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Skip the __exit__ cleanup code after closing WITH blocks
|
|
675
|
+
// Pattern: LOAD_CONST None, LOAD_CONST None, LOAD_CONST None, CALL 2/3, POP_TOP
|
|
676
|
+
if (closedWithBlock) {
|
|
677
|
+
// Check if we're at the start of __exit__ cleanup code
|
|
678
|
+
const constObj = this.code.Current.ConstantObject?.object;
|
|
679
|
+
const isLoadingNone = this.code.Current.OpCodeID === this.OpCodes.LOAD_CONST_A &&
|
|
680
|
+
(constObj == null || constObj.ClassName === 'Py_None');
|
|
681
|
+
if (isLoadingNone) {
|
|
682
|
+
// Skip the cleanup pattern: LOAD_CONST None x3, CALL, CACHE*, POP_TOP
|
|
683
|
+
// Stop at any instruction that starts a new statement
|
|
684
|
+
let skipCount = 0;
|
|
685
|
+
while (this.code.HasInstructionsToProcess && skipCount < 20) {
|
|
686
|
+
const instr = this.code.Current;
|
|
687
|
+
// Stop at instructions that start new statements
|
|
688
|
+
if (instr.OpCodeID === this.OpCodes.BEFORE_WITH ||
|
|
689
|
+
instr.OpCodeID === this.OpCodes.PUSH_NULL ||
|
|
690
|
+
instr.OpCodeID === this.OpCodes.RETURN_VALUE ||
|
|
691
|
+
instr.OpCodeID === this.OpCodes.RETURN_CONST_A ||
|
|
692
|
+
instr.OpCodeID === this.OpCodes.LOAD_FAST_A ||
|
|
693
|
+
instr.OpCodeID === this.OpCodes.LOAD_FAST_BORROW_A ||
|
|
694
|
+
instr.OpCodeID === this.OpCodes.LOAD_FAST_CHECK_A ||
|
|
695
|
+
instr.OpCodeID === this.OpCodes.LOAD_NAME_A ||
|
|
696
|
+
instr.OpCodeID === this.OpCodes.LOAD_GLOBAL_A ||
|
|
697
|
+
instr.OpCodeID === this.OpCodes.STORE_FAST_A ||
|
|
698
|
+
instr.OpCodeID === this.OpCodes.STORE_NAME_A) {
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
if (g_cliArgs?.debug) {
|
|
702
|
+
console.log(`[Pre-handler] Skipping WITH cleanup: ${instr.InstructionName} at ${instr.Offset}`);
|
|
703
|
+
}
|
|
704
|
+
skipCount++;
|
|
705
|
+
this.code.GoNext();
|
|
706
|
+
}
|
|
707
|
+
// Don't continue - let the current instruction be processed
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (this.code.Current.OpCodeID in PycDecompiler.opCodeHandlers)
|
|
712
|
+
{
|
|
713
|
+
PycDecompiler.opCodeHandlers[this.code.Current.OpCodeID].call(this);
|
|
714
|
+
} else {
|
|
715
|
+
console.error(`Unsupported opcode ${this.code.Current.InstructionName} at pos ${this.code.Current.Offset}\n`);
|
|
716
|
+
this.cleanBuild = false;
|
|
717
|
+
let node = new AST.ASTNodeList(this.defBlock.nodes);
|
|
718
|
+
return node;
|
|
719
|
+
}
|
|
720
|
+
this.closeEndedBlocks();
|
|
721
|
+
this.else_pop = [AST.ASTBlock.BlockType.Else,
|
|
722
|
+
AST.ASTBlock.BlockType.If,
|
|
723
|
+
AST.ASTBlock.BlockType.Elif
|
|
724
|
+
].includes(this.curBlock.blockType)
|
|
725
|
+
&& (this.curBlock.end == this.code.Next?.Offset);
|
|
726
|
+
|
|
727
|
+
} catch (ex) {
|
|
728
|
+
console.error(`EXCEPTION for OpCode ${this.code.Current.InstructionName} (${this.code.Current.Argument}) at offset ${this.code.Current.Offset} in code object '${this.object.Name}', file offset ${this.object.codeOffset + this.code.Current.Offset} : ${ex.message}\n\n`);
|
|
729
|
+
if (global.g_cliArgs?.debug) {
|
|
730
|
+
console.error('Stack trace:', ex.stack);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (this.blocks.length > 1) {
|
|
736
|
+
if (g_cliArgs?.debug) {
|
|
737
|
+
console.error(`Warning: block stack is not empty${this.blocks.length} blocks.\n`);
|
|
738
|
+
console.error('Remaining blocks in stack:');
|
|
739
|
+
for (let i = 0; i < this.blocks.length; i++) {
|
|
740
|
+
let blk = this.blocks[i];
|
|
741
|
+
console.error(` [${i}] ${blk.type_str} (${blk.start}-${blk.end}) nodes=${blk.nodes.length}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
while (this.blocks.length > 1) {
|
|
746
|
+
let tmp = this.blocks.pop();
|
|
747
|
+
|
|
748
|
+
// Set end offset for blocks that were never closed properly
|
|
749
|
+
if (tmp.end === 0 || tmp.end === undefined) {
|
|
750
|
+
tmp.end = this.code.Current?.Offset || this.object.CodeSize;
|
|
751
|
+
if (g_cliArgs?.debug) {
|
|
752
|
+
console.error(` Setting end=${tmp.end} for unclosed ${tmp.type_str}(${tmp.start}-${tmp.end})`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (g_cliArgs?.debug) {
|
|
757
|
+
console.error(`Appending ${tmp.type_str} (nodes=${tmp.nodes.length}) to ${this.blocks.top().type_str}`);
|
|
758
|
+
}
|
|
759
|
+
this.blocks.top().append(tmp);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
this.cleanBuild = true;
|
|
764
|
+
let mainNode = new AST.ASTNodeList(this.defBlock.nodes);
|
|
765
|
+
return mainNode;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
transformExceptionGroups(root) {
|
|
769
|
+
const merged = this.mergeOrphanedEgHandlers(root);
|
|
770
|
+
if (merged) {
|
|
771
|
+
// Still run light cleanup to drop residual artifacts
|
|
772
|
+
this.cleanupExceptBlocks(root);
|
|
773
|
+
this.pruneEmptyExcepts(root);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Breadth-first traversal to avoid deep recursion; skip on large graphs
|
|
778
|
+
const MAX_VISITED = 20000;
|
|
779
|
+
const queue = [root];
|
|
780
|
+
const visited = new WeakSet();
|
|
781
|
+
let visitedCount = 0;
|
|
782
|
+
while (queue.length) {
|
|
783
|
+
const node = queue.shift();
|
|
784
|
+
if (!node || visited.has(node)) continue;
|
|
785
|
+
visited.add(node);
|
|
786
|
+
if (++visitedCount > MAX_VISITED) break;
|
|
787
|
+
if (node instanceof AST.ASTNodeList) {
|
|
788
|
+
queue.push(...node.list);
|
|
789
|
+
} else if (node instanceof AST.ASTStore) {
|
|
790
|
+
queue.push(node.src);
|
|
791
|
+
} else if (node instanceof AST.ASTFunction) {
|
|
792
|
+
queue.push(node.code?.object?.SourceCode);
|
|
793
|
+
} else if (node instanceof AST.ASTBlock || node instanceof AST.ASTCondBlock) {
|
|
794
|
+
if (node instanceof AST.ASTCondBlock) {
|
|
795
|
+
this.tryConvertExceptStar(node);
|
|
796
|
+
}
|
|
797
|
+
queue.push(...(node.nodes || []));
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (visitedCount <= MAX_VISITED) {
|
|
802
|
+
this.flattenNestedExcepts(root);
|
|
803
|
+
this.removeEgHelperArtifacts(root);
|
|
804
|
+
this.cleanupExceptBlocks(root);
|
|
805
|
+
this.pruneEmptyExcepts(root);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
mergeOrphanedEgHandlers(root) {
|
|
810
|
+
if (!(root instanceof AST.ASTNodeList)) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const tryIdx = root.list.findIndex(n => n instanceof AST.ASTBlock && n.blockType === AST.ASTBlock.BlockType.Try);
|
|
815
|
+
if (tryIdx < 0 || tryIdx >= root.list.length - 1) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const trailing = root.list.slice(tryIdx + 1);
|
|
820
|
+
// Skip leading cleanup nodes (e = None / del e / __exception__ artifacts)
|
|
821
|
+
let firstStoreIdx = trailing.findIndex(n =>
|
|
822
|
+
n instanceof AST.ASTStore &&
|
|
823
|
+
n.src instanceof AST.ASTCall &&
|
|
824
|
+
n.src.func instanceof AST.ASTName &&
|
|
825
|
+
n.src.func.name === "__check_eg_match__");
|
|
826
|
+
|
|
827
|
+
if (firstStoreIdx < 0) {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const first = trailing[firstStoreIdx];
|
|
832
|
+
const matchType = first.src.pparams?.[1] || null;
|
|
833
|
+
|
|
834
|
+
if (!matchType) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const aliasName = first instanceof AST.ASTStore ? (first.dest || new AST.ASTName("e")) : new AST.ASTName("e");
|
|
839
|
+
|
|
840
|
+
// Drop cleanup artifacts; keep only meaningful body (e.g., print)
|
|
841
|
+
const filtered = trailing.slice(firstStoreIdx).filter(node => {
|
|
842
|
+
if (node instanceof AST.ASTSubscr && !node.name && !node.key) {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
if (node instanceof AST.ASTReturn && node.value instanceof AST.ASTObject && node.value.m_obj == null) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
if (node instanceof AST.ASTStore &&
|
|
849
|
+
node.src instanceof AST.ASTCall &&
|
|
850
|
+
node.src.func?.name === "__check_eg_match__") {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
if (aliasName && node instanceof AST.ASTStore &&
|
|
854
|
+
node.dest?.name === aliasName.name &&
|
|
855
|
+
node.src instanceof AST.ASTNone) {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
if (aliasName && node instanceof AST.ASTDelete &&
|
|
859
|
+
node.value?.name === aliasName.name) {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
return true;
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
if (!filtered.length) {
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const tryBlock = root.list[tryIdx];
|
|
870
|
+
const sanitizeBody = (nodes) => {
|
|
871
|
+
return (nodes || []).filter(node => {
|
|
872
|
+
if (node instanceof AST.ASTStore &&
|
|
873
|
+
node.src instanceof AST.ASTCall &&
|
|
874
|
+
node.src.func?.name === "__check_eg_match__") {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
if (node instanceof AST.ASTStore &&
|
|
878
|
+
node.dest?.name === aliasName.name &&
|
|
879
|
+
node.src instanceof AST.ASTNone) {
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
if (node instanceof AST.ASTDelete &&
|
|
883
|
+
node.value?.name === aliasName.name) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
if (node instanceof AST.ASTSubscr && !node.name && !node.key) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
if (node instanceof AST.ASTReturn && node.value instanceof AST.ASTObject && node.value.m_obj == null) {
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
const condStr = node.condition?.codeFragment?.()?.toString?.();
|
|
893
|
+
if (node instanceof AST.ASTCondBlock && typeof condStr === 'string' && condStr.includes('__prep_reraise_star__')) {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
if (node instanceof AST.ASTReturn && (!node.value || node.value instanceof AST.ASTNone)) {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
return true;
|
|
900
|
+
});
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// Capture existing ValueError body if present
|
|
904
|
+
const existingExcepts = (tryBlock.nodes || []).filter(n => n instanceof AST.ASTCondBlock && n.blockType === AST.ASTBlock.BlockType.Except);
|
|
905
|
+
const valueErrBlock = existingExcepts.find(b => {
|
|
906
|
+
const cond = b.condition;
|
|
907
|
+
const condName = cond instanceof AST.ASTStore ? cond.src : cond;
|
|
908
|
+
return condName instanceof AST.ASTName && condName.name === "ValueError";
|
|
909
|
+
});
|
|
910
|
+
let valueErrBody = sanitizeBody(valueErrBlock?.nodes || []);
|
|
911
|
+
const typeErrBody = sanitizeBody(filtered);
|
|
912
|
+
|
|
913
|
+
const callNode = (tryBlock.nodes || []).find(n => n instanceof AST.ASTCall);
|
|
914
|
+
const newBlocks = [];
|
|
915
|
+
if (callNode) {
|
|
916
|
+
newBlocks.push(callNode);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// If the ValueError handler lost its body (common on 3.11), pull following siblings in the try
|
|
920
|
+
if (valueErrBlock && valueErrBody.length === 0) {
|
|
921
|
+
const tryNodes = tryBlock.nodes || [];
|
|
922
|
+
const valIdx = tryNodes.indexOf(valueErrBlock);
|
|
923
|
+
if (valIdx >= 0) {
|
|
924
|
+
const tail = [];
|
|
925
|
+
for (let i = valIdx + 1; i < tryNodes.length; i++) {
|
|
926
|
+
const n = tryNodes[i];
|
|
927
|
+
if (n instanceof AST.ASTCondBlock && n.blockType === AST.ASTBlock.BlockType.Except) {
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
tail.push(n);
|
|
931
|
+
}
|
|
932
|
+
if (tail.length) {
|
|
933
|
+
// Remove tail from try block
|
|
934
|
+
tryBlock.m_nodes = tryNodes.slice(0, valIdx + 1);
|
|
935
|
+
valueErrBody = sanitizeBody(tail);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const makeExcept = (typeName, body) => {
|
|
941
|
+
const cond = new AST.ASTStore(new AST.ASTName(typeName), aliasName);
|
|
942
|
+
cond.line = aliasName.line || cond.line;
|
|
943
|
+
const blk = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, -1, -1, cond, false);
|
|
944
|
+
blk.isExceptStar = true;
|
|
945
|
+
blk.nodes.push(...body);
|
|
946
|
+
return blk;
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
if (valueErrBody.length) {
|
|
950
|
+
newBlocks.push(makeExcept("ValueError", valueErrBody));
|
|
951
|
+
}
|
|
952
|
+
if (typeErrBody.length) {
|
|
953
|
+
newBlocks.push(makeExcept(matchType?.name || matchType?.toString?.() || "Exception", typeErrBody));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (newBlocks.length) {
|
|
957
|
+
tryBlock.m_nodes = newBlocks;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Remove only the consumed trailing nodes (including any leading cleanup we skipped over)
|
|
961
|
+
root.list.splice(tryIdx + 1 + firstStoreIdx, trailing.length - firstStoreIdx);
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
tryConvertExceptStar(block) {
|
|
966
|
+
if (!this.isExceptStarPattern(block)) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const compare = block.condition;
|
|
971
|
+
const call = compare.left;
|
|
972
|
+
let aliasStore = null;
|
|
973
|
+
for (let child of block.nodes || []) {
|
|
974
|
+
if (child instanceof AST.ASTStore && child.src === call && child.dest instanceof AST.ASTName) {
|
|
975
|
+
aliasStore = child;
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (!aliasStore || !(aliasStore.dest instanceof AST.ASTName)) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const aliasName = aliasStore.dest;
|
|
985
|
+
const typeNode = call.pparams?.[1] || new AST.ASTName("ExceptionGroup");
|
|
986
|
+
|
|
987
|
+
const filtered = (block.nodes || []).filter(child => child !== aliasStore);
|
|
988
|
+
const cleaned = filtered.filter(child => {
|
|
989
|
+
if (child instanceof AST.ASTStore && child.dest?.name === aliasName.name && child.src instanceof AST.ASTNone) {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
if (child instanceof AST.ASTDelete && child.value?.name === aliasName.name) {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
return true;
|
|
996
|
+
});
|
|
997
|
+
block.m_nodes = cleaned;
|
|
998
|
+
|
|
999
|
+
const condStore = new AST.ASTStore(typeNode, aliasName);
|
|
1000
|
+
condStore.line = block.line;
|
|
1001
|
+
block.condition = condStore;
|
|
1002
|
+
block.m_blockType = AST.ASTBlock.BlockType.Except;
|
|
1003
|
+
block.isExceptStar = true;
|
|
1004
|
+
block.negative = false;
|
|
1005
|
+
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
isExceptStarPattern(block) {
|
|
1009
|
+
if (!(block instanceof AST.ASTCondBlock)) {
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (![AST.ASTBlock.BlockType.If, AST.ASTBlock.BlockType.Elif].includes(block.blockType)) {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const cond = block.condition;
|
|
1018
|
+
if (!(cond instanceof AST.ASTCompare)) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
if (![AST.ASTCompare.CompareOp.Is, AST.ASTCompare.CompareOp.IsNot].includes(cond.op)) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
if (!(cond.right instanceof AST.ASTNone)) {
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
const left = cond.left;
|
|
1028
|
+
if (!(left instanceof AST.ASTCall)) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
if (left.func?.name !== '__check_eg_match__') {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
removeEgHelperArtifacts(node) {
|
|
1038
|
+
// Temporarily disabled to avoid deep recursion issues
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
flattenNestedExcepts(root) {
|
|
1043
|
+
const queue = [{node: root, parentArr: null}];
|
|
1044
|
+
const visited = new WeakSet();
|
|
1045
|
+
while (queue.length) {
|
|
1046
|
+
const {node, parentArr} = queue.shift();
|
|
1047
|
+
if (!node || visited.has(node)) continue;
|
|
1048
|
+
visited.add(node);
|
|
1049
|
+
|
|
1050
|
+
const children = node instanceof AST.ASTNodeList ? node.list :
|
|
1051
|
+
(node instanceof AST.ASTBlock || node instanceof AST.ASTCondBlock) ? node.nodes :
|
|
1052
|
+
null;
|
|
1053
|
+
|
|
1054
|
+
if (Array.isArray(children)) {
|
|
1055
|
+
for (let idx = 0; idx < children.length; idx++) {
|
|
1056
|
+
const child = children[idx];
|
|
1057
|
+
if (child instanceof AST.ASTCondBlock && child.blockType === AST.ASTBlock.BlockType.Except) {
|
|
1058
|
+
const hoisted = [];
|
|
1059
|
+
child.m_nodes = (child.nodes || []).filter(n => {
|
|
1060
|
+
if (n instanceof AST.ASTCondBlock && n.blockType === AST.ASTBlock.BlockType.Except) {
|
|
1061
|
+
hoisted.push(n);
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
return true;
|
|
1065
|
+
});
|
|
1066
|
+
if (hoisted.length && Array.isArray(children)) {
|
|
1067
|
+
children.splice(idx + 1, 0, ...hoisted);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
// Enqueue after potential mutation to avoid skipping
|
|
1072
|
+
children.forEach(ch => queue.push({node: ch, parentArr: children}));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
cleanupExceptBlocks(node) {
|
|
1078
|
+
if (!node) return;
|
|
1079
|
+
const stripExcept = (blk) => {
|
|
1080
|
+
if (blk.blockType !== AST.ASTBlock.BlockType.Except) return;
|
|
1081
|
+
let aliasName = null;
|
|
1082
|
+
let matchType = null;
|
|
1083
|
+
blk.m_nodes = (blk.nodes || []).filter(child => {
|
|
1084
|
+
if (child.blockType === AST.ASTBlock.BlockType.Except) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
if (child instanceof AST.ASTCondBlock && child.blockType == AST.ASTBlock.BlockType.Except) {
|
|
1088
|
+
return false; // drop nested excepts
|
|
1089
|
+
}
|
|
1090
|
+
if (child instanceof AST.ASTStore &&
|
|
1091
|
+
child.src instanceof AST.ASTCall &&
|
|
1092
|
+
child.src.func?.name === "__check_eg_match__") {
|
|
1093
|
+
aliasName = child.dest instanceof AST.ASTName ? child.dest : aliasName;
|
|
1094
|
+
matchType = child.src.pparams?.[1] || matchType;
|
|
1095
|
+
return false; // header already encodes condition
|
|
1096
|
+
}
|
|
1097
|
+
if (child instanceof AST.ASTStore &&
|
|
1098
|
+
child.dest instanceof AST.ASTName &&
|
|
1099
|
+
child.src instanceof AST.ASTNone) {
|
|
1100
|
+
aliasName = child.dest;
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
if (child instanceof AST.ASTDelete && aliasName && child.value?.name === aliasName.name) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
if (child instanceof AST.ASTCondBlock) {
|
|
1107
|
+
const condStr = child.condition?.codeFragment?.();
|
|
1108
|
+
if (typeof condStr === 'string' && condStr.includes("__exception__<EXCEPTION MATCH>")) {
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (child instanceof AST.ASTSubscr) {
|
|
1113
|
+
const frag = child.codeFragment?.();
|
|
1114
|
+
const fragStr = frag?.toString?.() || '';
|
|
1115
|
+
if (fragStr.includes('__exception__')) {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return true;
|
|
1120
|
+
});
|
|
1121
|
+
if (aliasName) {
|
|
1122
|
+
const cond = matchType || blk.condition || new AST.ASTName("Exception");
|
|
1123
|
+
blk.condition = new AST.ASTStore(cond, aliasName);
|
|
1124
|
+
if (matchType) {
|
|
1125
|
+
blk.isExceptStar = true;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (blk.m_nodes.length > 3) {
|
|
1129
|
+
// Trim runaway nested bodies: keep calls and raises
|
|
1130
|
+
blk.m_nodes = blk.m_nodes.filter(ch => ch instanceof AST.ASTCall || ch instanceof AST.ASTRaise);
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
// Limit traversal breadth/depth to avoid runaway recursion on malformed trees
|
|
1135
|
+
const queue = [{n: node, d: 0}];
|
|
1136
|
+
const visitedNodes = new WeakSet();
|
|
1137
|
+
const MAX_DEPTH = 512;
|
|
1138
|
+
const MAX_VISITED = 5000;
|
|
1139
|
+
let visitedCount = 0;
|
|
1140
|
+
while (queue.length) {
|
|
1141
|
+
const {n: cur, d} = queue.shift();
|
|
1142
|
+
if (!cur || visitedNodes.has(cur)) continue;
|
|
1143
|
+
visitedNodes.add(cur);
|
|
1144
|
+
if (++visitedCount > MAX_VISITED) break;
|
|
1145
|
+
if (d > MAX_DEPTH) continue;
|
|
1146
|
+
if (cur instanceof AST.ASTBlock || cur instanceof AST.ASTCondBlock) {
|
|
1147
|
+
stripExcept(cur);
|
|
1148
|
+
(cur.nodes || []).forEach(ch => queue.push({n: ch, d: d + 1}));
|
|
1149
|
+
} else if (cur instanceof AST.ASTNodeList) {
|
|
1150
|
+
cur.list.forEach(ch => queue.push({n: ch, d: d + 1}));
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
pruneEmptyExcepts(root) {
|
|
1156
|
+
const queue = [{node: root, parentArr: null, idx: -1}];
|
|
1157
|
+
const visited = new WeakSet();
|
|
1158
|
+
const isCleanupNode = (node, aliasName) => {
|
|
1159
|
+
if (!node) return false;
|
|
1160
|
+
if (node instanceof AST.ASTStore &&
|
|
1161
|
+
node.src instanceof AST.ASTCall &&
|
|
1162
|
+
node.src.func?.name === "__check_eg_match__") {
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
if (node instanceof AST.ASTDelete) {
|
|
1166
|
+
return !aliasName || node.value?.name === aliasName.name;
|
|
1167
|
+
}
|
|
1168
|
+
if (node instanceof AST.ASTStore && node.dest?.name === aliasName?.name && node.src instanceof AST.ASTNone) {
|
|
1169
|
+
return true;
|
|
1170
|
+
}
|
|
1171
|
+
let fragStr = '';
|
|
1172
|
+
try {
|
|
1173
|
+
const frag = node?.codeFragment?.();
|
|
1174
|
+
fragStr = frag?.toString?.() || '';
|
|
1175
|
+
} catch (e) {
|
|
1176
|
+
// Skip nodes that fail to render
|
|
1177
|
+
}
|
|
1178
|
+
if (typeof fragStr === 'string' && fragStr.includes('__exception__')) {
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
if (node instanceof AST.ASTKeyword && node.word === AST.ASTKeyword.Word.Pass) {
|
|
1182
|
+
return true;
|
|
1183
|
+
}
|
|
1184
|
+
return false;
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
// Helper to check if a node is except* cleanup at parent level
|
|
1188
|
+
const isExceptStarCleanup = (node, aliasNames) => {
|
|
1189
|
+
if (!node || !aliasNames || aliasNames.size === 0) return false;
|
|
1190
|
+
// Never treat full blocks as cleanup; only simple statements can be cleanup artifacts.
|
|
1191
|
+
if (node instanceof AST.ASTBlock || node instanceof AST.ASTCondBlock) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
// del e
|
|
1195
|
+
if (node instanceof AST.ASTDelete && aliasNames.has(node.value?.name)) {
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
1198
|
+
// e = None
|
|
1199
|
+
if (node instanceof AST.ASTStore && aliasNames.has(node.dest?.name)) {
|
|
1200
|
+
const val = node.src;
|
|
1201
|
+
if (val instanceof AST.ASTNone || val?.object?.ClassName === 'Py_None') {
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
// __exception__[...]
|
|
1206
|
+
let fragStr = '';
|
|
1207
|
+
try {
|
|
1208
|
+
const frag = node?.codeFragment?.();
|
|
1209
|
+
fragStr = frag?.toString?.() || '';
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
// Skip nodes that fail to render
|
|
1212
|
+
}
|
|
1213
|
+
if (typeof fragStr === 'string' && fragStr.includes('__exception__')) {
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
return false;
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
// First pass: collect except* alias names from try blocks and filter sibling cleanup
|
|
1220
|
+
const filterExceptStarCleanup = (children) => {
|
|
1221
|
+
if (!Array.isArray(children)) return;
|
|
1222
|
+
const aliasNames = new Set();
|
|
1223
|
+
// Collect alias names from except* handlers in try blocks
|
|
1224
|
+
for (const child of children) {
|
|
1225
|
+
if (child instanceof AST.ASTBlock && child.blockType === AST.ASTBlock.BlockType.Try) {
|
|
1226
|
+
for (const tryChild of (child.nodes || [])) {
|
|
1227
|
+
if (tryChild instanceof AST.ASTCondBlock &&
|
|
1228
|
+
tryChild.blockType === AST.ASTBlock.BlockType.Except &&
|
|
1229
|
+
tryChild.isExceptStar) {
|
|
1230
|
+
const alias = tryChild.condition instanceof AST.ASTStore ? tryChild.condition.dest?.name : null;
|
|
1231
|
+
if (alias) aliasNames.add(alias);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// Filter cleanup nodes at this level
|
|
1237
|
+
if (aliasNames.size > 0) {
|
|
1238
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
1239
|
+
if (isExceptStarCleanup(children[i], aliasNames)) {
|
|
1240
|
+
children.splice(i, 1);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
while (queue.length) {
|
|
1247
|
+
const {node, parentArr, idx} = queue.shift();
|
|
1248
|
+
if (!node || visited.has(node)) continue;
|
|
1249
|
+
visited.add(node);
|
|
1250
|
+
|
|
1251
|
+
if (node instanceof AST.ASTCondBlock && node.blockType === AST.ASTBlock.BlockType.Except) {
|
|
1252
|
+
const alias = node.condition instanceof AST.ASTStore ? node.condition.dest : null;
|
|
1253
|
+
const filtered = (node.nodes || []).filter(n => !isCleanupNode(n, alias));
|
|
1254
|
+
|
|
1255
|
+
// If an except handler has been reduced to empty but still has a condition, keep it with 'pass'
|
|
1256
|
+
// to preserve the handler rather than dropping it entirely.
|
|
1257
|
+
if (node.condition && filtered.length === 0) {
|
|
1258
|
+
node.m_nodes = [new AST.ASTKeyword(AST.ASTKeyword.Word.Pass)];
|
|
1259
|
+
} else if (filtered.length !== (node.nodes || []).length) {
|
|
1260
|
+
node.m_nodes = filtered;
|
|
1261
|
+
}
|
|
1262
|
+
if (Array.isArray(parentArr) && (!node.nodes || node.nodes.length === 0)) {
|
|
1263
|
+
parentArr.splice(idx, 1);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const children = node instanceof AST.ASTNodeList ? node.list :
|
|
1269
|
+
(node instanceof AST.ASTBlock || node instanceof AST.ASTCondBlock) ? node.nodes :
|
|
1270
|
+
null;
|
|
1271
|
+
if (Array.isArray(children)) {
|
|
1272
|
+
// Filter except* cleanup at this level before enqueueing
|
|
1273
|
+
filterExceptStarCleanup(children);
|
|
1274
|
+
children.forEach((ch, i) => queue.push({node: ch, parentArr: children, idx: i}));
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
isPrepReraiseBlock(block) {
|
|
1280
|
+
if (!(block instanceof AST.ASTCondBlock)) {
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
if (block.blockType != AST.ASTBlock.BlockType.If) {
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
const cond = block.condition;
|
|
1287
|
+
if (!(cond instanceof AST.ASTCompare)) {
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
if (!(cond.right instanceof AST.ASTNone)) {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
const call = cond.left;
|
|
1294
|
+
if (!(call instanceof AST.ASTCall) || call.func?.name !== '__prep_reraise_star__') {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
if (!block.nodes || block.nodes.length !== 1) {
|
|
1298
|
+
return false;
|
|
1299
|
+
}
|
|
1300
|
+
const bodyCall = block.nodes[0];
|
|
1301
|
+
return bodyCall instanceof AST.ASTCall && bodyCall.func?.name === '__prep_reraise_star__';
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
isEgCleanupElseBlock(block, prev) {
|
|
1305
|
+
if (!(block instanceof AST.ASTBlock)) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
if (block.blockType != AST.ASTBlock.BlockType.Else) {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
if (!(prev instanceof AST.ASTCondBlock) || prev.blockType != AST.ASTBlock.BlockType.Except || !prev.isExceptStar) {
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
const aliasName = prev.condition instanceof AST.ASTStore ? prev.condition.dest?.name : null;
|
|
1315
|
+
if (global.g_cliArgs?.debug) {
|
|
1316
|
+
console.log(`[EG] alias for cleanup check: ${aliasName}`);
|
|
1317
|
+
}
|
|
1318
|
+
if (!aliasName) {
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
const nodes = block.nodes || [];
|
|
1322
|
+
for (const node of nodes) {
|
|
1323
|
+
if (!this.isEgCleanupNode(node, aliasName)) {
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (global.g_cliArgs?.debug) {
|
|
1328
|
+
console.log(`[EG] Removing cleanup else block for alias ${aliasName}`);
|
|
1329
|
+
}
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
isEgCleanupNode(node, aliasName) {
|
|
1334
|
+
if (!node) {
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
if (node instanceof AST.ASTStore && node.dest?.name === aliasName) {
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
if (node instanceof AST.ASTDelete && node.value?.name === aliasName) {
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
1343
|
+
if (node instanceof AST.ASTName && node.name?.startsWith('##ERROR##')) {
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
if (node instanceof AST.ASTSubscr || node.constructor?.name === 'ASTSubscr') {
|
|
1347
|
+
const fragmentValue = node.codeFragment?.();
|
|
1348
|
+
const fragmentStr = typeof fragmentValue === 'string' ? fragmentValue : fragmentValue?.toString?.();
|
|
1349
|
+
if (typeof fragmentStr === 'string' && fragmentStr.includes('##ERROR##')) {
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (global.g_cliArgs?.debug) {
|
|
1354
|
+
console.log(`[EG] Not a cleanup node: ${node.constructor?.name} -> ${node.codeFragment?.()}`);
|
|
1355
|
+
}
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
rewriteClassDefinitions(root) {
|
|
1360
|
+
if (!(root instanceof AST.ASTNodeList)) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const hasDataclass = this.astHasDataclassImport(root);
|
|
1365
|
+
|
|
1366
|
+
for (const node of root.list) {
|
|
1367
|
+
if (!(node instanceof AST.ASTStore)) {
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (node.src instanceof AST.ASTCall && node.src.func instanceof AST.ASTClass &&
|
|
1372
|
+
this.isPlainClassCall(node.src)) {
|
|
1373
|
+
node.src = node.src.func;
|
|
1374
|
+
this.cleanupClassBody(node.src);
|
|
1375
|
+
if (hasDataclass) {
|
|
1376
|
+
node.addDecorator(new AST.ASTName('dataclass'));
|
|
1377
|
+
}
|
|
1378
|
+
} else if (node.src instanceof AST.ASTClass) {
|
|
1379
|
+
this.cleanupClassBody(node.src);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
isPlainClassCall(call) {
|
|
1385
|
+
if (!(call.func instanceof AST.ASTClass)) {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
const hasParams = (call.pparams && call.pparams.length > 0) ||
|
|
1389
|
+
(call.kwparams && call.kwparams.length > 0) ||
|
|
1390
|
+
call.hasVar || call.hasKw;
|
|
1391
|
+
return !hasParams;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
astHasDataclassImport(root) {
|
|
1395
|
+
if (!(root instanceof AST.ASTNodeList)) {
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
return root.list.some(node => {
|
|
1400
|
+
if (!(node instanceof AST.ASTImport)) {
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
const moduleName = node.name?.codeFragment?.();
|
|
1404
|
+
if (moduleName !== 'dataclasses') {
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
return node.stores?.some?.(store => store.src?.codeFragment?.() === 'dataclass');
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
cleanupClassBody(classNode) {
|
|
1412
|
+
if (!(classNode instanceof AST.ASTClass)) {
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
const codeObject = classNode.code?.func?.code?.object || classNode.code?.code?.object;
|
|
1416
|
+
const body = codeObject?.SourceCode;
|
|
1417
|
+
if (!body?.list) {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const filtered = [];
|
|
1422
|
+
for (const stmt of body.list) {
|
|
1423
|
+
if (this.isSyntheticClassAssignment(stmt)) {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
filtered.push(stmt);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
if (filtered.length === 1 && filtered[0] instanceof AST.ASTKeyword && filtered[0].word === AST.ASTKeyword.Word.Pass) {
|
|
1430
|
+
filtered.length = 0;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
body.list.length = 0;
|
|
1434
|
+
Array.prototype.push.apply(body.list, filtered);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
isSyntheticClassAssignment(node) {
|
|
1438
|
+
if (!(node instanceof AST.ASTStore)) {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
if (!(node.dest instanceof AST.ASTName)) {
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
const name = node.dest.name;
|
|
1445
|
+
return name === '__module__' ||
|
|
1446
|
+
name === '__qualname__' ||
|
|
1447
|
+
name === '__classcell__' ||
|
|
1448
|
+
name === '__firstlineno__' ||
|
|
1449
|
+
name === '__type_params__' ||
|
|
1450
|
+
name === '__static_attributes__' ||
|
|
1451
|
+
name === '.generic_base' ||
|
|
1452
|
+
name === '.type_params';
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
removeNullSentinelComparisons(root) {
|
|
1456
|
+
if (!root) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (root instanceof AST.ASTNodeList) {
|
|
1460
|
+
this.pruneNullSentinels(root.list);
|
|
1461
|
+
} else if (root instanceof AST.ASTBlock) {
|
|
1462
|
+
this.pruneNullSentinels(root.nodes);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
removeDuplicateReturns(root) {
|
|
1467
|
+
const prune = (nodes, visited = new Set()) => {
|
|
1468
|
+
if (!Array.isArray(nodes) || visited.has(nodes)) {
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
visited.add(nodes);
|
|
1472
|
+
for (let i = nodes.length - 1; i > 0; i--) {
|
|
1473
|
+
const cur = nodes[i];
|
|
1474
|
+
const prev = nodes[i - 1];
|
|
1475
|
+
if (cur instanceof AST.ASTReturn && prev instanceof AST.ASTReturn) {
|
|
1476
|
+
const curFrag = cur.codeFragment?.()?.toString?.();
|
|
1477
|
+
const prevFrag = prev.codeFragment?.()?.toString?.();
|
|
1478
|
+
if (curFrag === prevFrag) {
|
|
1479
|
+
nodes.splice(i, 1);
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (cur instanceof AST.ASTNodeList) {
|
|
1484
|
+
prune(cur.list, visited);
|
|
1485
|
+
} else if (cur instanceof AST.ASTBlock || cur instanceof AST.ASTCondBlock) {
|
|
1486
|
+
prune(cur.nodes, visited);
|
|
1487
|
+
} else if (cur instanceof AST.ASTStore && cur.src instanceof AST.ASTFunction) {
|
|
1488
|
+
prune(cur.src.code?.object?.SourceCode?.list, visited);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
if (root instanceof AST.ASTNodeList) {
|
|
1494
|
+
prune(root.list);
|
|
1495
|
+
} else if (root instanceof AST.ASTBlock) {
|
|
1496
|
+
prune(root.nodes);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
dedupeExceptHandlers(root) {
|
|
1501
|
+
const queue = [root];
|
|
1502
|
+
const visited = new WeakSet();
|
|
1503
|
+
while (queue.length) {
|
|
1504
|
+
const node = queue.shift();
|
|
1505
|
+
if (!node || visited.has(node)) continue;
|
|
1506
|
+
visited.add(node);
|
|
1507
|
+
|
|
1508
|
+
if (node instanceof AST.ASTBlock && node.blockType === AST.ASTBlock.BlockType.Try) {
|
|
1509
|
+
const nodes = node.nodes || [];
|
|
1510
|
+
// Remove trailing cleanup else blocks (exception-group artifacts).
|
|
1511
|
+
if (nodes.length >= 2) {
|
|
1512
|
+
const last = nodes[nodes.length - 1];
|
|
1513
|
+
const prev = nodes[nodes.length - 2];
|
|
1514
|
+
if (this.isEgCleanupElseBlock(last, prev)) {
|
|
1515
|
+
nodes.pop();
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// Drop trivial generic except handlers that only contain pass/cleanup.
|
|
1520
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
1521
|
+
const blk = nodes[i];
|
|
1522
|
+
if (!(blk instanceof AST.ASTCondBlock) || blk.blockType !== AST.ASTBlock.BlockType.Except) {
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
const condNode = blk.condition instanceof AST.ASTStore ? blk.condition.src : blk.condition;
|
|
1526
|
+
const condText = (typeof condNode?.codeFragment === 'function' ? condNode.codeFragment()?.toString?.() : null) || condNode?.name;
|
|
1527
|
+
const body = blk.nodes || [];
|
|
1528
|
+
const isTrivialBody = body.length === 0 ||
|
|
1529
|
+
body.every(n => n instanceof AST.ASTKeyword);
|
|
1530
|
+
if (isTrivialBody && (condText === "Exception" || condText === undefined)) {
|
|
1531
|
+
nodes.splice(i, 1);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Deduplicate consecutive except/except* handlers with identical condition and body.
|
|
1536
|
+
for (let i = nodes.length - 1; i > 0; i--) {
|
|
1537
|
+
const cur = nodes[i];
|
|
1538
|
+
const prev = nodes[i - 1];
|
|
1539
|
+
const bothExcept = cur instanceof AST.ASTCondBlock &&
|
|
1540
|
+
prev instanceof AST.ASTCondBlock &&
|
|
1541
|
+
cur.blockType === AST.ASTBlock.BlockType.Except &&
|
|
1542
|
+
prev.blockType === AST.ASTBlock.BlockType.Except;
|
|
1543
|
+
if (!bothExcept) {
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
const condA = cur.condition?.codeFragment?.()?.toString?.();
|
|
1547
|
+
const condB = prev.condition?.codeFragment?.()?.toString?.();
|
|
1548
|
+
if (condA !== condB) {
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (!!cur.isExceptStar !== !!prev.isExceptStar) {
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
const bodyA = cur.codeFragment?.()?.toString?.();
|
|
1555
|
+
const bodyB = prev.codeFragment?.()?.toString?.();
|
|
1556
|
+
if (bodyA && bodyB && bodyA === bodyB) {
|
|
1557
|
+
nodes.splice(i, 1);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const children = node instanceof AST.ASTNodeList ? node.list
|
|
1563
|
+
: node instanceof AST.ASTBlock || node instanceof AST.ASTCondBlock ? node.nodes
|
|
1564
|
+
: node instanceof AST.ASTStore && node.src instanceof AST.ASTFunction ? node.src.code?.object?.SourceCode?.list
|
|
1565
|
+
: null;
|
|
1566
|
+
if (Array.isArray(children)) {
|
|
1567
|
+
queue.push(...children);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
pruneNullSentinels(nodes, visited = new Set()) {
|
|
1573
|
+
if (!Array.isArray(nodes)) {
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (visited.has(nodes)) {
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
visited.add(nodes);
|
|
1580
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
1581
|
+
const node = nodes[i];
|
|
1582
|
+
if (this.isNullSentinelBlock(node)) {
|
|
1583
|
+
nodes.splice(i, 1);
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
if (node instanceof AST.ASTNodeList) {
|
|
1587
|
+
this.pruneNullSentinels(node.list, visited);
|
|
1588
|
+
} else if (node instanceof AST.ASTBlock) {
|
|
1589
|
+
this.pruneNullSentinels(node.nodes, visited);
|
|
1590
|
+
} else if (node instanceof AST.ASTStore && node.src instanceof AST.ASTFunction) {
|
|
1591
|
+
this.removeNullSentinelComparisons(node.src.code?.object?.SourceCode);
|
|
1592
|
+
} else if (node instanceof AST.ASTStore && node.src instanceof AST.ASTClass) {
|
|
1593
|
+
this.removeNullSentinelComparisons(node.src.code?.func?.code?.object?.SourceCode);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
isNullSentinelBlock(node) {
|
|
1599
|
+
if (!(node instanceof AST.ASTCondBlock)) {
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
const condition = node.condition;
|
|
1603
|
+
|
|
1604
|
+
// Drop degenerate IF blocks with no condition and empty body (often leftover from with cleanup tests)
|
|
1605
|
+
const emptyBody = !node.nodes || node.nodes.length === 0 ||
|
|
1606
|
+
node.nodes.every(child => child instanceof AST.ASTKeyword && child.word === AST.ASTKeyword.Word.Pass);
|
|
1607
|
+
if (!condition) {
|
|
1608
|
+
return emptyBody;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (!(condition instanceof AST.ASTCompare)) {
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
const leftStr = condition.left?.codeFragment?.()?.toString?.().trim?.();
|
|
1615
|
+
const isNullLiteral = leftStr === 'null';
|
|
1616
|
+
const comparesNone = condition.right instanceof AST.ASTNone;
|
|
1617
|
+
return isNullLiteral && comparesNone && emptyBody;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
wrapFunctionExceptionGroups(root) {
|
|
1621
|
+
if (!(root instanceof AST.ASTNodeList)) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Also handle module-level exception group patterns
|
|
1626
|
+
this.rewriteExceptionGroupsInList(root);
|
|
1627
|
+
|
|
1628
|
+
for (const node of root.list) {
|
|
1629
|
+
if (node instanceof AST.ASTStore && node.src instanceof AST.ASTFunction) {
|
|
1630
|
+
this.rewriteExceptionGroupsInList(node.src.code?.object?.SourceCode);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
rewriteExceptionGroupsInList(listNode) {
|
|
1636
|
+
if (!listNode?.list || listNode.list.length === 0) {
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const nodes = listNode.list;
|
|
1641
|
+
const firstExceptIdx = nodes.findIndex(node => this.isExceptStarBlock(node) || this.isPlainExceptBlock(node));
|
|
1642
|
+
if (firstExceptIdx === -1) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
let hoistedPrefix = [];
|
|
1647
|
+
let tryBlock = null;
|
|
1648
|
+
let handlerNodes = [];
|
|
1649
|
+
|
|
1650
|
+
// Reuse the nearest preceding try block if it has meaningful body; otherwise build a new one.
|
|
1651
|
+
if (firstExceptIdx > 0) {
|
|
1652
|
+
for (let i = firstExceptIdx - 1; i >= 0; i--) {
|
|
1653
|
+
if (nodes[i] instanceof AST.ASTBlock && nodes[i].blockType === AST.ASTBlock.BlockType.Try) {
|
|
1654
|
+
// Prefer try blocks with real body; skip empty placeholders.
|
|
1655
|
+
const hasBody = (nodes[i].nodes || []).length > 0;
|
|
1656
|
+
if (!hasBody) {
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
hoistedPrefix = nodes.slice(0, i);
|
|
1660
|
+
tryBlock = nodes[i];
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (!tryBlock) {
|
|
1667
|
+
let startIdx = 0;
|
|
1668
|
+
while (startIdx < firstExceptIdx && this.isEgHoistableSetupNode(nodes[startIdx])) {
|
|
1669
|
+
hoistedPrefix.push(nodes[startIdx]);
|
|
1670
|
+
startIdx++;
|
|
1671
|
+
}
|
|
1672
|
+
tryBlock = new AST.ASTBlock(AST.ASTBlock.BlockType.Try, nodes[startIdx]?.start || nodes[0]?.start || 0, 0, true);
|
|
1673
|
+
tryBlock.nodes.push(...nodes.slice(startIdx, firstExceptIdx));
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (tryBlock?.nodes?.length === 1 &&
|
|
1677
|
+
tryBlock.nodes[0] instanceof AST.ASTBlock &&
|
|
1678
|
+
tryBlock.nodes[0].blockType === AST.ASTBlock.BlockType.Try) {
|
|
1679
|
+
tryBlock = tryBlock.nodes[0];
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Pull handler blocks out of the try body if they were embedded there
|
|
1683
|
+
if (tryBlock?.nodes?.length) {
|
|
1684
|
+
const filteredBody = [];
|
|
1685
|
+
for (const stmt of tryBlock.nodes) {
|
|
1686
|
+
if (stmt instanceof AST.ASTCondBlock && stmt.blockType === AST.ASTBlock.BlockType.Except) {
|
|
1687
|
+
handlerNodes.push(stmt);
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
filteredBody.push(stmt);
|
|
1691
|
+
}
|
|
1692
|
+
tryBlock.m_nodes = filteredBody;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// Collapse nested try wrappers with no handlers
|
|
1696
|
+
let body = tryBlock?.nodes || [];
|
|
1697
|
+
while (body.length === 1 &&
|
|
1698
|
+
body[0] instanceof AST.ASTBlock &&
|
|
1699
|
+
body[0].blockType === AST.ASTBlock.BlockType.Try) {
|
|
1700
|
+
const inner = body[0];
|
|
1701
|
+
const hasHandlers = (inner.nodes || []).some(n => n instanceof AST.ASTCondBlock && n.blockType === AST.ASTBlock.BlockType.Except);
|
|
1702
|
+
if (hasHandlers) {
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1705
|
+
tryBlock.m_nodes = inner.nodes || [];
|
|
1706
|
+
body = tryBlock.m_nodes;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// Drop implicit "return None" from the end of the try body
|
|
1710
|
+
const lastNode = tryBlock.nodes[tryBlock.nodes.length - 1];
|
|
1711
|
+
const lastValStr = lastNode?.value?.codeFragment?.();
|
|
1712
|
+
if (lastNode instanceof AST.ASTReturn &&
|
|
1713
|
+
(lastNode.value instanceof AST.ASTNone || lastValStr === "None" || lastValStr === undefined)) {
|
|
1714
|
+
tryBlock.nodes.pop();
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
const rewritten = [...hoistedPrefix, tryBlock, ...handlerNodes];
|
|
1718
|
+
// Heuristic: wrap stray TypeError print cleanup into an except* TypeError handler (seen on 3.11)
|
|
1719
|
+
const consumed = new Set();
|
|
1720
|
+
for (let i = firstExceptIdx; i < nodes.length; i++) {
|
|
1721
|
+
const node = nodes[i];
|
|
1722
|
+
if (consumed.has(i)) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
const fragment = typeof node?.codeFragment === 'function' ? node.codeFragment()?.toString?.() : '';
|
|
1726
|
+
if (node instanceof AST.ASTCall && typeof fragment === 'string' && fragment.includes('TypeError') && fragment.includes('{e}')) {
|
|
1727
|
+
const cond = new AST.ASTStore(new AST.ASTName('TypeError'), new AST.ASTName('e'));
|
|
1728
|
+
const cb = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, node.start || 0, node.end || 0);
|
|
1729
|
+
cb.condition = cond;
|
|
1730
|
+
cb.isExceptStar = true;
|
|
1731
|
+
cb.m_nodes = [node];
|
|
1732
|
+
rewritten.push(cb);
|
|
1733
|
+
// Skip trailing cleanup nodes for e
|
|
1734
|
+
let j = i + 1;
|
|
1735
|
+
while (j < nodes.length) {
|
|
1736
|
+
const next = nodes[j];
|
|
1737
|
+
if (next instanceof AST.ASTStore && next.dest?.name === 'e') {
|
|
1738
|
+
consumed.add(j);
|
|
1739
|
+
j++;
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
if (next instanceof AST.ASTDelete && next.value?.name === 'e') {
|
|
1743
|
+
consumed.add(j);
|
|
1744
|
+
j++;
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
break;
|
|
1748
|
+
}
|
|
1749
|
+
consumed.add(i);
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
if (global.g_cliArgs?.debug && node instanceof AST.ASTBlock && node.blockType == AST.ASTBlock.BlockType.Else) {
|
|
1753
|
+
const prev = rewritten[rewritten.length - 1];
|
|
1754
|
+
console.log(`[EG] Evaluating else cleanup candidate after ${prev?.constructor?.name}:${prev?.blockType}`);
|
|
1755
|
+
}
|
|
1756
|
+
if (this.isEgCleanupElseBlock(node, rewritten[rewritten.length - 1])) {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
if (this.isPrepReraiseBlock(node)) {
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
if (node instanceof AST.ASTCondBlock &&
|
|
1763
|
+
node.blockType === AST.ASTBlock.BlockType.Except &&
|
|
1764
|
+
(!node.nodes || node.nodes.every(ch => {
|
|
1765
|
+
const frag = ch?.codeFragment?.()?.toString?.() || "";
|
|
1766
|
+
return frag === "pass" || frag.includes("__exception__") || frag.includes("##ERROR##");
|
|
1767
|
+
}))) {
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
if (typeof node?.codeFragment === 'function') {
|
|
1771
|
+
const frag = node.codeFragment();
|
|
1772
|
+
const fragStr = frag?.toString?.() || "";
|
|
1773
|
+
if (fragStr.includes("##ERROR##")) {
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
rewritten.push(node);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
const flattened = rewritten.map(n => {
|
|
1781
|
+
if (n instanceof AST.ASTBlock &&
|
|
1782
|
+
n.blockType === AST.ASTBlock.BlockType.Try &&
|
|
1783
|
+
n.nodes?.length === 1 &&
|
|
1784
|
+
n.nodes[0] instanceof AST.ASTBlock &&
|
|
1785
|
+
n.nodes[0].blockType === AST.ASTBlock.BlockType.Try) {
|
|
1786
|
+
const inner = n.nodes[0];
|
|
1787
|
+
const hasHandlers = (inner.nodes || []).some(ch => ch instanceof AST.ASTCondBlock && ch.blockType === AST.ASTBlock.BlockType.Except);
|
|
1788
|
+
if (!hasHandlers) {
|
|
1789
|
+
return inner;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return n;
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
const deduped = [];
|
|
1796
|
+
const seenHandlers = new Set();
|
|
1797
|
+
for (const node of flattened) {
|
|
1798
|
+
if (node instanceof AST.ASTCondBlock && node.blockType === AST.ASTBlock.BlockType.Except) {
|
|
1799
|
+
const condStr = node.condition?.codeFragment?.()?.toString?.() || "";
|
|
1800
|
+
const key = `${node.isExceptStar ? "star" : "plain"}:${condStr}`;
|
|
1801
|
+
if (seenHandlers.has(key)) {
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
seenHandlers.add(key);
|
|
1805
|
+
}
|
|
1806
|
+
deduped.push(node);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
listNode.list.length = 0;
|
|
1810
|
+
Array.prototype.push.apply(listNode.list, deduped);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
rewriteGenericWrappers(root) {
|
|
1814
|
+
if (!(root instanceof AST.ASTNodeList)) {
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
for (let i = 0; i < root.list.length; i++) {
|
|
1818
|
+
const node = root.list[i];
|
|
1819
|
+
if (!(node instanceof AST.ASTStore)) {
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
const call = node.src;
|
|
1823
|
+
if (!(call instanceof AST.ASTCall)) {
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
const func = call.func;
|
|
1827
|
+
if (!(func instanceof AST.ASTFunction)) {
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
const wrapperName = func.code?.object?.Name || "";
|
|
1831
|
+
if (!wrapperName.startsWith("<generic parameters of ")) {
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// If the call already returns a function/class directly, unwrap it
|
|
1836
|
+
if (func.typeParams?.length || func.annotations && Object.keys(func.annotations).length) {
|
|
1837
|
+
node.m_src = func;
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
const targetName = node.dest?.name || node.dest?.codeFragment?.();
|
|
1842
|
+
const codeConsts = func.code?.object?.Consts?.Value || [];
|
|
1843
|
+
const innerCodeObj = codeConsts.find(c => c?.ClassName === 'Py_CodeObject');
|
|
1844
|
+
if (!innerCodeObj) {
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
// Extract type parameters: pick uppercase-like strings excluding target name
|
|
1848
|
+
const typeParams = codeConsts
|
|
1849
|
+
.filter(c => c?.ClassName === 'Py_String')
|
|
1850
|
+
.map(c => c.Value)
|
|
1851
|
+
.filter(v => /^[A-Z][A-Za-z0-9_]*$/.test(v || '') && v !== targetName);
|
|
1852
|
+
|
|
1853
|
+
// Decompile inner code object to get body
|
|
1854
|
+
const innerDecompiler = new PycDecompiler(innerCodeObj);
|
|
1855
|
+
const innerBody = innerDecompiler.decompile();
|
|
1856
|
+
innerCodeObj.SourceCode = innerBody;
|
|
1857
|
+
const astObj = new AST.ASTObject(innerCodeObj);
|
|
1858
|
+
|
|
1859
|
+
const isClassLike = targetName && targetName[0] === targetName[0]?.toUpperCase?.();
|
|
1860
|
+
|
|
1861
|
+
if (!isClassLike) {
|
|
1862
|
+
const fn = new AST.ASTFunction(astObj);
|
|
1863
|
+
fn.annotations = innerDecompiler.funcAnnotations || fn.annotations;
|
|
1864
|
+
fn.typeParams = typeParams;
|
|
1865
|
+
node.m_src = fn;
|
|
1866
|
+
} else {
|
|
1867
|
+
const classFunc = new AST.ASTFunction(astObj);
|
|
1868
|
+
const bases = new AST.ASTTuple([]);
|
|
1869
|
+
const cls = new AST.ASTClass(classFunc, bases, new AST.ASTName(targetName));
|
|
1870
|
+
cls.typeParams = typeParams;
|
|
1871
|
+
node.m_src = cls;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// Simplify calls that simply wrap a generic wrapper function
|
|
1876
|
+
for (let i = 0; i < root.list.length; i++) {
|
|
1877
|
+
const node = root.list[i];
|
|
1878
|
+
if (node instanceof AST.ASTStore && node.src instanceof AST.ASTCall && node.src.func instanceof AST.ASTFunction && (node.src.pparams?.length || 0) === 0) {
|
|
1879
|
+
node.m_src = node.src.func;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
isExceptStarBlock(node) {
|
|
1885
|
+
return node instanceof AST.ASTCondBlock && node.blockType == AST.ASTBlock.BlockType.Except && node.isExceptStar;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
isPlainExceptBlock(node) {
|
|
1889
|
+
return node instanceof AST.ASTCondBlock && node.blockType == AST.ASTBlock.BlockType.Except;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
isEgHoistableSetupNode(node) {
|
|
1893
|
+
if (!(node instanceof AST.ASTStore)) {
|
|
1894
|
+
return false;
|
|
1895
|
+
}
|
|
1896
|
+
if (!(node.dest instanceof AST.ASTName)) {
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
return node.src instanceof AST.ASTNone ||
|
|
1900
|
+
node.src instanceof AST.ASTObject ||
|
|
1901
|
+
node.src instanceof AST.ASTFunction;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
/**
|
|
1905
|
+
* Recursively checks if a block ends with a terminating keyword (break/continue/return)
|
|
1906
|
+
* Used to prevent generating additional continue statements after breaks in nested blocks
|
|
1907
|
+
*/
|
|
1908
|
+
hasTerminatingKeyword(block) {
|
|
1909
|
+
if (!block || !block.nodes || block.nodes.length == 0) {
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
let lastNode = block.nodes[block.nodes.length - 1];
|
|
1914
|
+
|
|
1915
|
+
// Check if last node is a terminating keyword
|
|
1916
|
+
if (lastNode instanceof AST.ASTKeyword) {
|
|
1917
|
+
return [AST.ASTKeyword.Word.Break,
|
|
1918
|
+
AST.ASTKeyword.Word.Continue,
|
|
1919
|
+
AST.ASTKeyword.Word.Return].includes(lastNode.word);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// Check if last node is a block - recurse into it
|
|
1923
|
+
if (lastNode instanceof AST.ASTBlock) {
|
|
1924
|
+
return this.hasTerminatingKeyword(lastNode);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// Check if last node is a return statement
|
|
1928
|
+
if (lastNode instanceof AST.ASTReturn) {
|
|
1929
|
+
return true;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/**
|
|
1936
|
+
* Look ahead from current COPY after LOAD to detect match pattern
|
|
1937
|
+
* Strategy: Find next POP_JUMP_IF_FALSE, check if its target is another COPY
|
|
1938
|
+
* This confirms match/case pattern before first case is processed
|
|
1939
|
+
*/
|
|
1940
|
+
lookAheadForMatchPattern() {
|
|
1941
|
+
// Match patterns only exist in Python 3.10+
|
|
1942
|
+
if (this.object.Reader.versionCompare(3, 10) < 0) {
|
|
1943
|
+
return false;
|
|
1944
|
+
}
|
|
1945
|
+
if (!this.code.Next) {
|
|
1946
|
+
if (global.g_cliArgs?.debug) {
|
|
1947
|
+
console.log(`[LOOK-AHEAD] No next instruction`);
|
|
1948
|
+
}
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Find next POP_JUMP_IF_FALSE opcode (should be within ~10 instructions for literal patterns)
|
|
1953
|
+
let nextOp = this.code.Next;
|
|
1954
|
+
let jumpOp = null;
|
|
1955
|
+
|
|
1956
|
+
for (let i = 0; i < 10 && nextOp; i++) {
|
|
1957
|
+
if (nextOp.OpCodeID == this.OpCodes.POP_JUMP_IF_FALSE_A ||
|
|
1958
|
+
nextOp.OpCodeID == this.OpCodes.POP_JUMP_FORWARD_IF_FALSE_A) {
|
|
1959
|
+
jumpOp = nextOp;
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
nextOp = nextOp.Next;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
if (!jumpOp) {
|
|
1966
|
+
if (global.g_cliArgs?.debug) {
|
|
1967
|
+
console.log(`[LOOK-AHEAD] No POP_JUMP found within 10 instructions`);
|
|
1968
|
+
}
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
if (jumpOp.JumpTarget === undefined || jumpOp.JumpTarget === null) {
|
|
1973
|
+
if (global.g_cliArgs?.debug) {
|
|
1974
|
+
console.log(`[LOOK-AHEAD] POP_JUMP has no jump target`);
|
|
1975
|
+
}
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
if (global.g_cliArgs?.debug) {
|
|
1980
|
+
console.log(`[LOOK-AHEAD] Found POP_JUMP at offset ${jumpOp.Offset}, target=${jumpOp.JumpTarget}`);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// Find instruction at or after jump target offset
|
|
1984
|
+
// Note: JumpTarget might not match exact instruction offset due to cache instructions
|
|
1985
|
+
let targetOp = jumpOp.Next;
|
|
1986
|
+
|
|
1987
|
+
// Navigate forward from POP_JUMP to find instruction at target
|
|
1988
|
+
// Look for next COPY (should be within reasonable distance ~40-50 bytes)
|
|
1989
|
+
let searchLimit = 100; // Don't search too far
|
|
1990
|
+
let searchCount = 0;
|
|
1991
|
+
|
|
1992
|
+
while (targetOp && searchCount < searchLimit) {
|
|
1993
|
+
// Found a COPY opcode?
|
|
1994
|
+
if (targetOp.OpCodeID == this.OpCodes.DUP_TOP ||
|
|
1995
|
+
(targetOp.OpCodeID == this.OpCodes.COPY_A && targetOp.Argument == 1)) {
|
|
1996
|
+
|
|
1997
|
+
if (global.g_cliArgs?.debug) {
|
|
1998
|
+
console.log(`[LOOK-AHEAD] Found COPY at offset ${targetOp.Offset} after POP_JUMP`);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// Check if this COPY is preceded by LOAD (if yes, not a match pattern)
|
|
2002
|
+
let prevOp = targetOp.Prev;
|
|
2003
|
+
if (prevOp && (prevOp.OpCodeID == this.OpCodes.LOAD_FAST_A ||
|
|
2004
|
+
prevOp.OpCodeID == this.OpCodes.LOAD_NAME_A ||
|
|
2005
|
+
prevOp.OpCodeID == this.OpCodes.LOAD_GLOBAL_A)) {
|
|
2006
|
+
// This COPY is after LOAD → not the reuse pattern
|
|
2007
|
+
if (global.g_cliArgs?.debug) {
|
|
2008
|
+
console.log(`[LOOK-AHEAD] COPY at ${targetOp.Offset} has LOAD before → not match pattern`);
|
|
2009
|
+
}
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// This COPY has no LOAD before → match pattern!
|
|
2014
|
+
if (global.g_cliArgs?.debug) {
|
|
2015
|
+
console.log(`[LOOK-AHEAD] Match pattern detected! COPY at ${targetOp.Offset} has no prior LOAD`);
|
|
2016
|
+
}
|
|
2017
|
+
return true;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
targetOp = targetOp.Next;
|
|
2021
|
+
searchCount++;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
if (global.g_cliArgs?.debug) {
|
|
2025
|
+
console.log(`[LOOK-AHEAD] No COPY found after POP_JUMP within ${searchLimit} bytes`);
|
|
2026
|
+
}
|
|
2027
|
+
return false;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
module.exports = PycDecompiler;
|