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,1201 @@
|
|
|
1
|
+
const AST = require('../ast/ast_node');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reconstruct pattern from recorded operations
|
|
5
|
+
* @param {Array} patternOps - Array of pattern operations
|
|
6
|
+
* @returns {{pattern: ASTPattern, remainderOps: Array}} pattern plus leftover operations
|
|
7
|
+
*/
|
|
8
|
+
function reconstructPattern(patternOps) {
|
|
9
|
+
const ops = patternOps || [];
|
|
10
|
+
const consumed = new Set();
|
|
11
|
+
|
|
12
|
+
const markConsumed = (op) => {
|
|
13
|
+
if (op) {
|
|
14
|
+
consumed.add(op);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const buildResult = (pattern) => {
|
|
19
|
+
let remainderOps = ops.filter(op => !consumed.has(op));
|
|
20
|
+
|
|
21
|
+
// AS-pattern: leftover single STORE_FAST wraps existing pattern
|
|
22
|
+
const asStore = remainderOps.find(op => op.type === 'STORE_FAST');
|
|
23
|
+
if (asStore && pattern) {
|
|
24
|
+
markConsumed(asStore);
|
|
25
|
+
pattern = new AST.ASTPattern(AST.ASTPattern.PatternType.As, {
|
|
26
|
+
pattern,
|
|
27
|
+
name: asStore.name
|
|
28
|
+
});
|
|
29
|
+
remainderOps = ops.filter(op => !consumed.has(op));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {pattern, remainderOps};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (ops.length === 0) {
|
|
36
|
+
return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Find key operations
|
|
40
|
+
let matchSeqOp = ops.find(op => op.type === 'MATCH_SEQUENCE');
|
|
41
|
+
let hasMatchSeq = !!matchSeqOp;
|
|
42
|
+
let matchClassOp = ops.find(op => op.type === 'MATCH_CLASS');
|
|
43
|
+
let matchKeysOp = ops.find(op => op.type === 'MATCH_KEYS');
|
|
44
|
+
let matchMappingOp = ops.find(op => op.type === 'MATCH_MAPPING');
|
|
45
|
+
let unpackOp = ops.find(op => op.type === 'UNPACK_SEQUENCE');
|
|
46
|
+
let compareOp = ops.find(op => op.type === 'COMPARE');
|
|
47
|
+
let getLenOpIndex = ops.findIndex(op => op.type === 'GET_LEN');
|
|
48
|
+
|
|
49
|
+
// In 3.13+, MATCH_SEQUENCE is preceded by GET_LEN/COMPARE length checks.
|
|
50
|
+
if (hasMatchSeq && getLenOpIndex >= 0) {
|
|
51
|
+
markConsumed(ops[getLenOpIndex]);
|
|
52
|
+
const cmpAfterLen = ops.slice(getLenOpIndex + 1).find(op => op.type === 'COMPARE');
|
|
53
|
+
if (cmpAfterLen) {
|
|
54
|
+
markConsumed(cmpAfterLen);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (matchKeysOp || matchMappingOp) {
|
|
59
|
+
if (matchKeysOp) {
|
|
60
|
+
markConsumed(matchKeysOp);
|
|
61
|
+
}
|
|
62
|
+
if (matchMappingOp) {
|
|
63
|
+
markConsumed(matchMappingOp);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const keys = matchKeysOp?.keys || [];
|
|
67
|
+
const stores = ops.filter(op => op.type === 'STORE_FAST');
|
|
68
|
+
const entries = [];
|
|
69
|
+
|
|
70
|
+
let idx = 0;
|
|
71
|
+
for (; idx < keys.length; idx++) {
|
|
72
|
+
const keyNode = keys[idx];
|
|
73
|
+
let pattern = null;
|
|
74
|
+
if (stores[idx]) {
|
|
75
|
+
pattern = new AST.ASTPattern(AST.ASTPattern.PatternType.Variable, stores[idx].name);
|
|
76
|
+
markConsumed(stores[idx]);
|
|
77
|
+
} else {
|
|
78
|
+
pattern = new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_');
|
|
79
|
+
}
|
|
80
|
+
entries.push({key: keyNode, pattern});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Consume any extra stores associated with mapping binding
|
|
84
|
+
for (; idx < stores.length; idx++) {
|
|
85
|
+
markConsumed(stores[idx]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return buildResult(new AST.ASTPattern(
|
|
89
|
+
AST.ASTPattern.PatternType.Mapping,
|
|
90
|
+
entries
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (matchClassOp) {
|
|
95
|
+
markConsumed(matchClassOp);
|
|
96
|
+
const attributes = [];
|
|
97
|
+
const attrNames = matchClassOp.attrNames || [];
|
|
98
|
+
|
|
99
|
+
// Determine how many positional attributes are expected (fallback to opcode count)
|
|
100
|
+
let attrCount = attrNames.length;
|
|
101
|
+
if (!attrCount && matchClassOp.count) {
|
|
102
|
+
attrCount = matchClassOp.count;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Focus on operations that happen after UNPACK_SEQUENCE (if present)
|
|
106
|
+
let attrOpsStart = ops.indexOf(matchClassOp) + 1;
|
|
107
|
+
let attrOps = ops.slice(attrOpsStart);
|
|
108
|
+
const unpackIndex = attrOps.findIndex(op => op.type === 'UNPACK_SEQUENCE');
|
|
109
|
+
if (unpackIndex >= 0) {
|
|
110
|
+
attrOps = attrOps.slice(unpackIndex + 1);
|
|
111
|
+
if (unpackOp) {
|
|
112
|
+
markConsumed(unpackOp);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Handle SWAP 2 which reverses attribute evaluation order
|
|
117
|
+
let reverseOrder = false;
|
|
118
|
+
if (attrOps.length && attrOps[0].type === 'SWAP' && attrOps[0].depth === 2) {
|
|
119
|
+
markConsumed(attrOps[0]);
|
|
120
|
+
reverseOrder = true;
|
|
121
|
+
attrOps = attrOps.slice(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Collect COMPARE / STORE_FAST operations that correspond to attributes
|
|
125
|
+
const attributeOps = [];
|
|
126
|
+
for (const op of attrOps) {
|
|
127
|
+
if (op.type === 'COMPARE' || op.type === 'STORE_FAST') {
|
|
128
|
+
attributeOps.push(op);
|
|
129
|
+
markConsumed(op);
|
|
130
|
+
}
|
|
131
|
+
if (attrCount && attributeOps.length >= attrCount) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (reverseOrder) {
|
|
137
|
+
attributeOps.reverse();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let attrIndex = 0;
|
|
141
|
+
for (const op of attributeOps) {
|
|
142
|
+
if (attrCount && attrIndex >= attrCount) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (op.type === 'COMPARE') {
|
|
147
|
+
const literalPattern = new AST.ASTPattern(
|
|
148
|
+
AST.ASTPattern.PatternType.Literal,
|
|
149
|
+
op.right
|
|
150
|
+
);
|
|
151
|
+
const name = attrNames.length ? (attrNames[attrIndex] || `_attr${attrIndex}`) : null;
|
|
152
|
+
attributes.push({name, pattern: literalPattern});
|
|
153
|
+
} else if (op.type === 'STORE_FAST') {
|
|
154
|
+
const varPattern = new AST.ASTPattern(
|
|
155
|
+
AST.ASTPattern.PatternType.Variable,
|
|
156
|
+
op.name
|
|
157
|
+
);
|
|
158
|
+
const name = attrNames.length ? (attrNames[attrIndex] || op.name || `_attr${attrIndex}`) : null;
|
|
159
|
+
attributes.push({name, pattern: varPattern});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
attrIndex++;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// If there are declared attributes without recorded ops, fill them with wildcards
|
|
166
|
+
while (attrIndex < attrNames.length) {
|
|
167
|
+
attributes.push({
|
|
168
|
+
name: attrNames[attrIndex],
|
|
169
|
+
pattern: new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_')
|
|
170
|
+
});
|
|
171
|
+
attrIndex++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return buildResult(new AST.ASTPattern(
|
|
175
|
+
AST.ASTPattern.PatternType.Class,
|
|
176
|
+
{
|
|
177
|
+
classExpr: matchClassOp.classExpr,
|
|
178
|
+
attributes
|
|
179
|
+
}
|
|
180
|
+
));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Literal OR pattern: multiple COMPARE_OP checks in a row
|
|
184
|
+
const allCompareOps = ops.filter(op => op.type === 'COMPARE');
|
|
185
|
+
if (!hasMatchSeq && allCompareOps.length > 1) {
|
|
186
|
+
const orPatterns = [];
|
|
187
|
+
for (const cmp of allCompareOps) {
|
|
188
|
+
markConsumed(cmp);
|
|
189
|
+
if (cmp.right instanceof AST.ASTObject) {
|
|
190
|
+
orPatterns.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, cmp.right));
|
|
191
|
+
} else if (cmp.right instanceof AST.ASTName) {
|
|
192
|
+
orPatterns.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Variable, cmp.right.name));
|
|
193
|
+
} else {
|
|
194
|
+
orPatterns.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Or, orPatterns));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Literal pattern: Just COMPARE (no MATCH_SEQUENCE)
|
|
201
|
+
if (!hasMatchSeq && compareOp) {
|
|
202
|
+
markConsumed(compareOp);
|
|
203
|
+
// Pure literal pattern: case 1: or case "string":
|
|
204
|
+
// right operand is the literal value
|
|
205
|
+
if (compareOp.right instanceof AST.ASTObject) {
|
|
206
|
+
return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, compareOp.right));
|
|
207
|
+
}
|
|
208
|
+
// Fallback
|
|
209
|
+
return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!hasMatchSeq) {
|
|
213
|
+
// No MATCH_SEQUENCE and no COMPARE → wildcard
|
|
214
|
+
return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!unpackOp) {
|
|
218
|
+
// MATCH_SEQUENCE but no UNPACK → wildcard sequence match
|
|
219
|
+
return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Build sequence pattern with unpacked elements
|
|
223
|
+
let count = unpackOp.count;
|
|
224
|
+
let elements = [];
|
|
225
|
+
|
|
226
|
+
// Find element patterns after UNPACK_SEQUENCE
|
|
227
|
+
// Next COMPARE or STORE_FAST operations define elements
|
|
228
|
+
let unpackIndex = ops.findIndex(op => op.type === 'UNPACK_SEQUENCE');
|
|
229
|
+
let afterUnpack = ops.slice(unpackIndex + 1);
|
|
230
|
+
|
|
231
|
+
// Check for SWAP operation (indicates element order reversal)
|
|
232
|
+
let hasSwap = afterUnpack.length > 0 && afterUnpack[0].type === 'SWAP' && afterUnpack[0].depth === 2;
|
|
233
|
+
|
|
234
|
+
// Skip SWAP when processing elements
|
|
235
|
+
if (hasSwap) {
|
|
236
|
+
markConsumed(afterUnpack[0]);
|
|
237
|
+
afterUnpack = afterUnpack.slice(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Process operations to extract element patterns
|
|
241
|
+
let elementIndex = 0;
|
|
242
|
+
for (let i = 0; i < afterUnpack.length && elementIndex < count; i++) {
|
|
243
|
+
let op = afterUnpack[i];
|
|
244
|
+
|
|
245
|
+
if (op.type === 'COMPARE') {
|
|
246
|
+
// Literal pattern: value is compared
|
|
247
|
+
// right is ASTObject with the literal value
|
|
248
|
+
if (op.right instanceof AST.ASTObject) {
|
|
249
|
+
let literalValue = op.right;
|
|
250
|
+
elements.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, literalValue));
|
|
251
|
+
markConsumed(op);
|
|
252
|
+
elementIndex++;
|
|
253
|
+
}
|
|
254
|
+
} else if (op.type === 'STORE_FAST') {
|
|
255
|
+
// Variable pattern: value is captured
|
|
256
|
+
let varName = op.name;
|
|
257
|
+
elements.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Variable, varName));
|
|
258
|
+
markConsumed(op);
|
|
259
|
+
elementIndex++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Fill remaining with wildcards if needed
|
|
264
|
+
while (elements.length < count) {
|
|
265
|
+
elements.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_'));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// If SWAP was used, reverse element order
|
|
269
|
+
// SWAP 2 after UNPACK indicates elements were swapped to change check order
|
|
270
|
+
// We need to reverse to restore source pattern order
|
|
271
|
+
if (hasSwap) {
|
|
272
|
+
elements.reverse();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Create sequence pattern
|
|
276
|
+
markConsumed(matchSeqOp);
|
|
277
|
+
markConsumed(unpackOp);
|
|
278
|
+
return buildResult(new AST.ASTPattern(AST.ASTPattern.PatternType.Sequence, elements));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function hasUpcomingMatchCase() {
|
|
282
|
+
const nextOpCodeId = this.code.Next?.OpCodeID;
|
|
283
|
+
const nextNextOpCodeId = this.code.Next?.Next?.OpCodeID;
|
|
284
|
+
const matchOpcodes = [
|
|
285
|
+
this.OpCodes.MATCH_SEQUENCE,
|
|
286
|
+
this.OpCodes.MATCH_MAPPING,
|
|
287
|
+
this.OpCodes.MATCH_KEYS,
|
|
288
|
+
this.OpCodes.MATCH_CLASS_A
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
if (matchOpcodes.includes(nextOpCodeId)) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let isCopy = (nextOpCodeId == this.OpCodes.COPY_A && this.code.Next?.Argument == 1) ||
|
|
296
|
+
nextOpCodeId == this.OpCodes.DUP_TOP;
|
|
297
|
+
if (isCopy) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let nextIsPop = nextOpCodeId == this.OpCodes.POP_TOP;
|
|
302
|
+
let nextNextIsCopy = nextNextOpCodeId == this.OpCodes.COPY_A;
|
|
303
|
+
let nextNextIsMatch = nextNextOpCodeId == this.OpCodes.MATCH_SEQUENCE;
|
|
304
|
+
if (nextIsPop && (nextNextIsCopy || nextNextIsMatch)) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// NOP followed by code (no COMPARE_OP) = default case (case _:)
|
|
309
|
+
// This pattern appears in 3.10/3.11 after the last literal case
|
|
310
|
+
if (nextOpCodeId == this.OpCodes.NOP && this.currentMatch) {
|
|
311
|
+
// Check if this NOP is followed by executable code (not another pattern)
|
|
312
|
+
let afterNop = this.code.Next?.Next;
|
|
313
|
+
if (afterNop && afterNop.OpCodeID != this.OpCodes.COMPARE_OP_A &&
|
|
314
|
+
afterNop.OpCodeID != this.OpCodes.COPY_A &&
|
|
315
|
+
afterNop.OpCodeID != this.OpCodes.DUP_TOP) {
|
|
316
|
+
return true; // Default case ahead
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 3.13 literal-only match compilation: sequences of CACHE/LOAD_CONST/COMPARE_OP
|
|
321
|
+
let scan = this.code.Next;
|
|
322
|
+
let steps = 0;
|
|
323
|
+
while (scan && steps < 8) {
|
|
324
|
+
if (scan.OpCodeID == this.OpCodes.COMPARE_OP_A) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
if (scan.OpCodeID == this.OpCodes.LOAD_CONST_A ||
|
|
328
|
+
scan.OpCodeID == this.OpCodes.CACHE ||
|
|
329
|
+
scan.OpCodeID == this.OpCodes.COPY_A ||
|
|
330
|
+
scan.OpCodeID == this.OpCodes.POP_JUMP_IF_FALSE_A) {
|
|
331
|
+
scan = scan.Next;
|
|
332
|
+
steps++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let look = this.code.Next;
|
|
339
|
+
steps = 0;
|
|
340
|
+
while (look && steps < 50) {
|
|
341
|
+
if (matchOpcodes.includes(look.OpCodeID) || look.OpCodeID == this.OpCodes.POP_TOP) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
look = look.Next;
|
|
345
|
+
steps++;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function flushCurrentCaseBody() {
|
|
352
|
+
if (!this.currentCase) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let startIdx = this.caseBodyStartIndex || 0;
|
|
357
|
+
let bodyNodes = this.curBlock.nodes.slice(startIdx);
|
|
358
|
+
const {guard, bodyNodes: normalized} = extractGuardFromBody(bodyNodes);
|
|
359
|
+
const filtered = normalized.filter(node => !(node instanceof AST.ASTName) && !(node instanceof AST.ASTTuple));
|
|
360
|
+
|
|
361
|
+
if (global.g_cliArgs?.debug) {
|
|
362
|
+
const debugNodes = filtered.map(n => `${n?.constructor?.name || typeof n}:${n?.codeFragment ? n.codeFragment() : ''}`);
|
|
363
|
+
console.log(`[MATCH] Case nodes: ${debugNodes.join(' | ')}`);
|
|
364
|
+
if (guard) {
|
|
365
|
+
console.log(`[MATCH] Guard detected: ${guard.codeFragment?.() || guard.constructor?.name}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let body = new AST.ASTNodeList(filtered);
|
|
370
|
+
this.currentCase.m_body = body;
|
|
371
|
+
if (guard) {
|
|
372
|
+
this.currentCase.m_guard = guard;
|
|
373
|
+
}
|
|
374
|
+
this.currentMatch.addCase(this.currentCase);
|
|
375
|
+
this.curBlock.nodes.length = startIdx;
|
|
376
|
+
this.currentCase = null;
|
|
377
|
+
|
|
378
|
+
if (global.g_cliArgs?.debug) {
|
|
379
|
+
console.log(`[MATCH] Case body recorded with ${filtered.length} node(s)`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function beginMatchCaseFromPattern(options = {}) {
|
|
386
|
+
if (!this.currentMatch) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Close the previous case body when a new case starts
|
|
391
|
+
flushCurrentCaseBody.call(this);
|
|
392
|
+
|
|
393
|
+
const {pattern, remainderOps} = reconstructPattern(this.patternOps);
|
|
394
|
+
this.caseBodyStartIndex = this.curBlock?.nodes?.length || 0;
|
|
395
|
+
|
|
396
|
+
const initialGuard = guardFromPatternRemainder(remainderOps);
|
|
397
|
+
this.currentCase = new AST.ASTCase(pattern, null, initialGuard);
|
|
398
|
+
this.currentCase.line = options.line || this.code.Current.LineNo;
|
|
399
|
+
this.inMatchPattern = false;
|
|
400
|
+
this.patternOps = [];
|
|
401
|
+
|
|
402
|
+
if (global.g_cliArgs?.debug) {
|
|
403
|
+
console.log(`[MATCH] Starting case (${options.reason || 'pop_top'}) at offset ${this.code.Current.Offset}`);
|
|
404
|
+
console.log(` caseBodyStartIndex=${this.caseBodyStartIndex}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function finalizeMatchCase() {
|
|
411
|
+
if (!this.currentMatch) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
flushCurrentCaseBody.call(this);
|
|
416
|
+
|
|
417
|
+
// Normalize guard-only cases that lost their bodies due to literal/guard separation
|
|
418
|
+
const normalizeMatchCases = (matchNode) => {
|
|
419
|
+
if (!matchNode?.cases || matchNode.cases.length < 2) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const wildcardType = AST.ASTPattern.PatternType.Wildcard;
|
|
423
|
+
const cleaned = [];
|
|
424
|
+
for (let i = 0; i < matchNode.cases.length; i++) {
|
|
425
|
+
const cur = matchNode.cases[i];
|
|
426
|
+
const next = matchNode.cases[i + 1];
|
|
427
|
+
const curBodyLen = cur.body?.list?.length || 0;
|
|
428
|
+
const curHasGuard = !!cur.guard;
|
|
429
|
+
const nextIsWildcard = next && next.pattern?.type === wildcardType && !next.guard;
|
|
430
|
+
if (curHasGuard && curBodyLen === 0 && nextIsWildcard) {
|
|
431
|
+
// Move body from wildcard fallback into guarded case
|
|
432
|
+
cur.m_body = next.body;
|
|
433
|
+
i++; // Skip the wildcard case
|
|
434
|
+
}
|
|
435
|
+
// Skip empty wildcard cases (no guard, no body)
|
|
436
|
+
if (cur.pattern?.type === wildcardType && !cur.guard && curBodyLen === 0) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
cleaned.push(cur);
|
|
440
|
+
}
|
|
441
|
+
matchNode.m_cases = cleaned;
|
|
442
|
+
};
|
|
443
|
+
normalizeMatchCases(this.currentMatch);
|
|
444
|
+
|
|
445
|
+
if (global.g_cliArgs?.debug) {
|
|
446
|
+
console.log(`[MATCH] Match complete, appending ASTMatch to parent block`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (this.dataStack.length > 0 && this.dataStack.top() === this.matchSubject) {
|
|
450
|
+
this.dataStack.pop();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
let targetBlock = this.matchParentBlock || this.curBlock;
|
|
454
|
+
|
|
455
|
+
const preIndex = Math.max(0, this.matchPreNodesStart || 0);
|
|
456
|
+
targetBlock.nodes.length = preIndex;
|
|
457
|
+
targetBlock.append(this.currentMatch);
|
|
458
|
+
this.currentMatch = null;
|
|
459
|
+
this.matchSubject = null;
|
|
460
|
+
this.matchParentBlock = null;
|
|
461
|
+
this.matchPreNodesStart = 0;
|
|
462
|
+
this.caseBodyStartIndex = 0;
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function handleExecStmt() {
|
|
467
|
+
if (this.dataStack.top() instanceof AST.ASTChainStore) {
|
|
468
|
+
this.dataStack.pop();
|
|
469
|
+
}
|
|
470
|
+
let loc = this.dataStack.pop();
|
|
471
|
+
let glob = this.dataStack.pop();
|
|
472
|
+
let stmt = this.dataStack.pop();
|
|
473
|
+
let node = new AST.ASTExec(stmt, glob, loc);
|
|
474
|
+
node.line = this.code.Current.LineNo;
|
|
475
|
+
this.curBlock.append(node);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function handleFormatValueA() {
|
|
479
|
+
let conversion_flag = this.code.Current.Argument;
|
|
480
|
+
switch (conversion_flag) {
|
|
481
|
+
case AST.ASTFormattedValue.ConversionFlag.None:
|
|
482
|
+
case AST.ASTFormattedValue.ConversionFlag.Str:
|
|
483
|
+
case AST.ASTFormattedValue.ConversionFlag.Repr:
|
|
484
|
+
case AST.ASTFormattedValue.ConversionFlag.ASCII:
|
|
485
|
+
{
|
|
486
|
+
let val = this.dataStack.pop();
|
|
487
|
+
let node = new AST.ASTFormattedValue (val, conversion_flag, null);
|
|
488
|
+
node.line = this.code.Current.LineNo;
|
|
489
|
+
this.dataStack.push(node);
|
|
490
|
+
}
|
|
491
|
+
break;
|
|
492
|
+
case AST.ASTFormattedValue.ConversionFlag.FmtSpec:
|
|
493
|
+
{
|
|
494
|
+
let format_spec = this.dataStack.pop();
|
|
495
|
+
let val = this.dataStack.pop();
|
|
496
|
+
let node = new AST.ASTFormattedValue (val, conversion_flag, format_spec);
|
|
497
|
+
node.line = this.code.Current.LineNo;
|
|
498
|
+
this.dataStack.push(node);
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
default:
|
|
502
|
+
console.error(`Unsupported FORMAT_VALUE_A conversion flag: ${this.code.Current.Argument}\n`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function hasUpcomingStringBuild(context) {
|
|
507
|
+
const lookahead = [
|
|
508
|
+
context.OpCodes.BUILD_STRING_A,
|
|
509
|
+
context.OpCodes.BUILD_INTERPOLATION_A,
|
|
510
|
+
context.OpCodes.BUILD_TEMPLATE
|
|
511
|
+
];
|
|
512
|
+
const earlyStops = new Set([
|
|
513
|
+
context.OpCodes.RETURN_VALUE,
|
|
514
|
+
context.OpCodes.CALL_A,
|
|
515
|
+
context.OpCodes.CALL_KW_A,
|
|
516
|
+
context.OpCodes.CALL_FUNCTION_EX,
|
|
517
|
+
context.OpCodes.STORE_FAST_A,
|
|
518
|
+
context.OpCodes.STORE_NAME_A,
|
|
519
|
+
context.OpCodes.STORE_GLOBAL_A,
|
|
520
|
+
context.OpCodes.STORE_ATTR_A,
|
|
521
|
+
context.OpCodes.STORE_DEREF_A
|
|
522
|
+
]);
|
|
523
|
+
|
|
524
|
+
for (let step = 1; step <= 12; step++) {
|
|
525
|
+
const instr = context.code.PeekInstructionAtOffset(context.code.Current.Offset + step * 2);
|
|
526
|
+
if (!instr) {
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
if (lookahead.includes(instr.OpCodeID)) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
if (earlyStops.has(instr.OpCodeID)) {
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function handleFormatSimple() {
|
|
540
|
+
// Python 3.13+ FORMAT_SIMPLE: format TOS without spec (conversion may have been applied by CONVERT_VALUE)
|
|
541
|
+
const val = this.dataStack.pop();
|
|
542
|
+
|
|
543
|
+
let node;
|
|
544
|
+
if (val instanceof AST.ASTFormattedValue) {
|
|
545
|
+
node = val;
|
|
546
|
+
} else {
|
|
547
|
+
node = new AST.ASTFormattedValue(
|
|
548
|
+
val,
|
|
549
|
+
AST.ASTFormattedValue.ConversionFlag.None,
|
|
550
|
+
null
|
|
551
|
+
);
|
|
552
|
+
node.line = this.code.Current.LineNo;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (!hasUpcomingStringBuild(this)) {
|
|
556
|
+
const joined = new AST.ASTJoinedStr([node]);
|
|
557
|
+
joined.line = node.line;
|
|
558
|
+
this.dataStack.push(joined);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
this.dataStack.push(node);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function handleFormatWithSpec() {
|
|
566
|
+
// Python 3.14 FORMAT_WITH_SPEC: value already converted (optional) + format spec on stack.
|
|
567
|
+
const formatSpec = this.dataStack.pop();
|
|
568
|
+
const val = this.dataStack.pop();
|
|
569
|
+
|
|
570
|
+
let target;
|
|
571
|
+
if (val instanceof AST.ASTFormattedValue) {
|
|
572
|
+
target = val;
|
|
573
|
+
} else {
|
|
574
|
+
target = new AST.ASTFormattedValue(val, AST.ASTFormattedValue.ConversionFlag.None, null);
|
|
575
|
+
target.line = this.code.Current.LineNo;
|
|
576
|
+
}
|
|
577
|
+
target.m_format_spec = formatSpec;
|
|
578
|
+
if (!hasUpcomingStringBuild(this)) {
|
|
579
|
+
const joined = new AST.ASTJoinedStr([target]);
|
|
580
|
+
joined.line = target.line;
|
|
581
|
+
this.dataStack.push(joined);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
this.dataStack.push(target);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function handlePopTop() {
|
|
589
|
+
// Match/case: Detect transition from pattern checks to case body
|
|
590
|
+
const readyForWildcard = this.currentMatch && !this.inMatchPattern && (this.patternOps?.length || 0) === 0 && !hasUpcomingMatchCase.call(this);
|
|
591
|
+
if (this.inMatchPattern || readyForWildcard) {
|
|
592
|
+
if (global.g_cliArgs?.debug) {
|
|
593
|
+
console.log(`[POP_TOP] Evaluating match state at offset ${this.code.Current.Offset}, prev=${this.code.Prev?.InstructionName}`);
|
|
594
|
+
}
|
|
595
|
+
// Check if this is the success path (pattern matched)
|
|
596
|
+
// Two scenarios:
|
|
597
|
+
// 1. Sequence patterns: POP_JUMP → UNPACK/etc → POP_TOP (Prev is NOT jump)
|
|
598
|
+
// 2. Literal patterns: POP_JUMP → POP_TOP (Prev IS jump)
|
|
599
|
+
let prevIsJump = [
|
|
600
|
+
this.OpCodes.POP_JUMP_IF_FALSE_A,
|
|
601
|
+
this.OpCodes.POP_JUMP_FORWARD_IF_FALSE_A,
|
|
602
|
+
this.OpCodes.POP_JUMP_IF_TRUE_A,
|
|
603
|
+
this.OpCodes.JUMP_IF_FALSE_A
|
|
604
|
+
].includes(this.code.Prev?.OpCodeID);
|
|
605
|
+
|
|
606
|
+
const patternHasOps = (this.patternOps?.length || 0) > 0;
|
|
607
|
+
let isLiteralPattern = patternHasOps && !this.patternOps.some(op => ["MATCH_SEQUENCE","MATCH_CLASS","MATCH_MAPPING","MATCH_KEYS"].includes(op.type));
|
|
608
|
+
let shouldStartCase = readyForWildcard || !!this.inMatchPattern;
|
|
609
|
+
if (patternHasOps) {
|
|
610
|
+
shouldStartCase = true;
|
|
611
|
+
}
|
|
612
|
+
if (patternHasOps && isLiteralPattern && !prevIsJump) {
|
|
613
|
+
shouldStartCase = false;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (shouldStartCase) {
|
|
617
|
+
if (beginMatchCaseFromPattern.call(this, {reason: 'pop_top'})) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
} else if (global.g_cliArgs?.debug) {
|
|
621
|
+
console.log(`[POP_TOP] Skipping case start at offset ${this.code.Current.Offset} (literal=${isLiteralPattern}, prevIsJump=${prevIsJump})`);
|
|
622
|
+
console.log(` patternOps=${JSON.stringify(this.patternOps.map(op => op.type))}`);
|
|
623
|
+
}
|
|
624
|
+
// Consume POP_TOP related to match even if no case starts
|
|
625
|
+
if (this.dataStack.length > 0) {
|
|
626
|
+
this.dataStack.pop();
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (!(this.dataStack.top() instanceof AST.ASTComprehension) && [this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A, this.OpCodes.POP_JUMP_IF_FALSE_A, this.OpCodes.JUMP_IF_FALSE_A].includes(this.code.Prev?.OpCodeID)) {
|
|
632
|
+
if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except) {
|
|
633
|
+
// Skipping POP_TOP, POP_TOP, POP_TOP
|
|
634
|
+
if ([this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A].includes(this.code.Prev.OpCodeID)) {
|
|
635
|
+
if (this.code.Next?.OpCodeID == this.OpCodes.POP_TOP && this.code.Next?.Next?.OpCodeID == this.OpCodes.POP_TOP) {
|
|
636
|
+
this.code.GoNext(2);
|
|
637
|
+
}
|
|
638
|
+
} else if (this.code.Prev.OpCodeID == this.OpCodes.POP_JUMP_IF_FALSE_A) {
|
|
639
|
+
if ([this.OpCodes.STORE_NAME_A, this.OpCodes.STORE_FAST_A].includes(this.code.Next?.OpCodeID) && this.code.Next?.Next?.OpCodeID == this.OpCodes.POP_TOP) {
|
|
640
|
+
let exceptionTypeNode = this.curBlock.condition;
|
|
641
|
+
if (!(exceptionTypeNode instanceof AST.ASTName)) {
|
|
642
|
+
const typeName = exceptionTypeNode?.constructor?.name || 'null';
|
|
643
|
+
if (global.g_cliArgs?.debug) {
|
|
644
|
+
console.error(`Expected ASTName, but got ${typeName}`);
|
|
645
|
+
}
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
let exceptionName = new AST.ASTName(this.code.Next.Name);
|
|
649
|
+
exceptionName.line = this.code.Current.LineNo;
|
|
650
|
+
this.curBlock.condition = new AST.ASTStore(exceptionTypeNode, exceptionName);
|
|
651
|
+
this.code.GoNext(2);
|
|
652
|
+
}
|
|
653
|
+
} else if (this.code.Prev.OpCodeID == this.OpCodes.JUMP_IF_FALSE_A) {
|
|
654
|
+
if ( this.code.Next?.OpCodeID == this.OpCodes.POP_TOP && [this.OpCodes.STORE_NAME_A, this.OpCodes.STORE_FAST_A].includes(this.code.Next?.Next?.OpCodeID) && this.code.Next?.Next?.Next?.OpCodeID == this.OpCodes.POP_TOP) {
|
|
655
|
+
let exceptionTypeNode = this.curBlock.condition;
|
|
656
|
+
if (!(exceptionTypeNode instanceof AST.ASTName)) {
|
|
657
|
+
const typeName = exceptionTypeNode?.constructor?.name || 'null';
|
|
658
|
+
if (global.g_cliArgs?.debug) {
|
|
659
|
+
console.error(`Expected ASTName, but got ${typeName}`);
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
let exceptionName = new AST.ASTName(this.code.Next.Next.Name);
|
|
664
|
+
exceptionName.line = this.code.Current.LineNo;
|
|
665
|
+
this.curBlock.condition = new AST.ASTStore(exceptionTypeNode, exceptionName);
|
|
666
|
+
this.code.GoNext(2);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return;
|
|
671
|
+
} else if ([this.OpCodes.PRINT_ITEM_TO].includes(this.code.Prev.OpCodeID) && this.curBlock.nodes.top() instanceof AST.ASTPrint) {
|
|
672
|
+
let printNode = this.curBlock.nodes.top();
|
|
673
|
+
if (printNode.stream && printNode.stream == this.dataStack.top()) {
|
|
674
|
+
this.dataStack.pop();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
let value = this.dataStack.pop();
|
|
679
|
+
if (this.appendExceptionExprs && this.curBlock.blockType == AST.ASTBlock.BlockType.Except) {
|
|
680
|
+
// direct expression in handler (e.g., add_note call result is discarded)
|
|
681
|
+
if (value && !(value instanceof AST.ASTNone)) {
|
|
682
|
+
this.curBlock.append(value);
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (!this.curBlock.inited) {
|
|
687
|
+
if (this.curBlock.blockType == AST.ASTBlock.BlockType.With) {
|
|
688
|
+
this.curBlock.expr = value;
|
|
689
|
+
} else if (this.curBlock.blockType == AST.ASTBlock.BlockType.If && !this.curBlock.condition) {
|
|
690
|
+
this.curBlock.condition = value;
|
|
691
|
+
}
|
|
692
|
+
// Don't initialize For/AsyncFor blocks here - they are initialized by STORE_FAST/STORE_NAME when setting the index
|
|
693
|
+
if (this.curBlock.blockType != AST.ASTBlock.BlockType.For &&
|
|
694
|
+
this.curBlock.blockType != AST.ASTBlock.BlockType.AsyncFor) {
|
|
695
|
+
this.curBlock.init();
|
|
696
|
+
}
|
|
697
|
+
} else if (value == null || value.processed) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!(value instanceof AST.ASTObject)) {
|
|
702
|
+
this.curBlock.append(value);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (this.curBlock.blockType == AST.ASTBlock.BlockType.For
|
|
706
|
+
&& this.curBlock.comprehension) {
|
|
707
|
+
/* This relies on some really uncertain logic...
|
|
708
|
+
* If it's a comprehension, the only POP_TOP should be
|
|
709
|
+
* a call to append the iter to the list.
|
|
710
|
+
*/
|
|
711
|
+
if (value instanceof AST.ASTCall) {
|
|
712
|
+
let pparams = value.pparams;
|
|
713
|
+
if (!pparams.empty()) {
|
|
714
|
+
let res = pparams[0];
|
|
715
|
+
let node = new AST.ASTComprehension (res);
|
|
716
|
+
node.line = this.code.Current.LineNo;
|
|
717
|
+
this.dataStack.push(node);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function handlePrintExpr() {
|
|
724
|
+
processPrint.call(this);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function handlePrintItem() {
|
|
728
|
+
processPrint.call(this);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function processPrint() {
|
|
732
|
+
let printNode;
|
|
733
|
+
if (this.curBlock.nodes.length > 0 && this.curBlock.nodes.top() instanceof AST.ASTPrint) {
|
|
734
|
+
printNode = this.curBlock.nodes.top();
|
|
735
|
+
}
|
|
736
|
+
if (printNode && printNode.stream == null && !printNode.eol) {
|
|
737
|
+
printNode.add(this.dataStack.pop());
|
|
738
|
+
} else {
|
|
739
|
+
let node = new AST.ASTPrint(this.dataStack.pop());
|
|
740
|
+
node.line = this.code.Current.LineNo;
|
|
741
|
+
this.curBlock.append(node);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function handlePrintItemTo() {
|
|
746
|
+
let stream = this.dataStack.pop();
|
|
747
|
+
let printNode;
|
|
748
|
+
|
|
749
|
+
if (this.curBlock.nodes.length > 0 && this.curBlock.nodes.top() instanceof AST.ASTPrint) {
|
|
750
|
+
printNode = this.curBlock.nodes.top();
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (printNode && printNode.stream == stream && !printNode.eol) {
|
|
754
|
+
printNode.add(this.dataStack.pop());
|
|
755
|
+
} else {
|
|
756
|
+
let node = new AST.ASTPrint(this.dataStack.pop(), stream);
|
|
757
|
+
node.line = this.code.Current.LineNo;
|
|
758
|
+
this.curBlock.append(node);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function handlePrintNewline() {
|
|
763
|
+
let printNode;
|
|
764
|
+
if (!this.curBlock.empty() && this.curBlock.nodes.top() instanceof AST.ASTPrint)
|
|
765
|
+
printNode = this.curBlock.nodes.top();
|
|
766
|
+
if (printNode && printNode.stream == null && !printNode.eol)
|
|
767
|
+
printNode.eol = true;
|
|
768
|
+
else {
|
|
769
|
+
let node = new AST.ASTPrint();
|
|
770
|
+
node.line = this.code.Current.LineNo;
|
|
771
|
+
this.curBlock.append(node);
|
|
772
|
+
}
|
|
773
|
+
this.dataStack.pop();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function handlePrintNewlineTo() {
|
|
777
|
+
let stream = this.dataStack.pop();
|
|
778
|
+
|
|
779
|
+
let printNode;
|
|
780
|
+
if (!this.curBlock.empty() && this.curBlock.nodes.top() instanceof AST.ASTPrint) {
|
|
781
|
+
printNode = this.curBlock.nodes.top();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (printNode && printNode.stream == stream && !printNode.eol) {
|
|
785
|
+
printNode.eol = true;
|
|
786
|
+
} else {
|
|
787
|
+
let node = new AST.ASTPrint(null, stream);
|
|
788
|
+
node.line = this.code.Current.LineNo;
|
|
789
|
+
this.curBlock.append(node);
|
|
790
|
+
}
|
|
791
|
+
this.dataStack.pop();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function handleReturnGenerator() {
|
|
795
|
+
// Python 3.11+ RETURN_GENERATOR opcode
|
|
796
|
+
// Appears at the start of generator/async generator functions
|
|
797
|
+
// Creates the generator object - no action needed in decompiler
|
|
798
|
+
if (global.g_cliArgs?.debug) {
|
|
799
|
+
console.log(`[RETURN_GENERATOR] at offset ${this.code.Current.Offset} - generator function detected`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function handleInstrumentedReturnValueA() {
|
|
804
|
+
this.handleReturnValue();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function handleReturnValue() {
|
|
808
|
+
if (this.currentMatch) {
|
|
809
|
+
// Only start new case if no current case AND (not in pattern OR has unflushed pattern ops)
|
|
810
|
+
const hasUnflushedPattern = (this.patternOps?.length || 0) > 0;
|
|
811
|
+
const needsNewCase = !this.currentCase && (!this.inMatchPattern || hasUnflushedPattern);
|
|
812
|
+
if (needsNewCase) {
|
|
813
|
+
beginMatchCaseFromPattern.call(this, {reason: 'return'});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// CRITICAL: Close blocks that have ended before processing return
|
|
818
|
+
// Return statements often appear at block boundaries
|
|
819
|
+
while (this.curBlock.end > 0 &&
|
|
820
|
+
this.curBlock.end <= this.code.Current.Offset &&
|
|
821
|
+
this.curBlock.blockType != AST.ASTBlock.BlockType.Main &&
|
|
822
|
+
this.blocks.length > 1) {
|
|
823
|
+
|
|
824
|
+
if (global.g_cliArgs?.debug) {
|
|
825
|
+
console.log(`[handleReturnValue] Closing ended block ${this.curBlock.type_str}(${this.curBlock.start}-${this.curBlock.end}) at offset ${this.code.Current.Offset}`);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
let closedBlock = this.blocks.pop();
|
|
829
|
+
this.curBlock = this.blocks.top();
|
|
830
|
+
this.curBlock.append(closedBlock);
|
|
831
|
+
|
|
832
|
+
if (global.g_cliArgs?.debug) {
|
|
833
|
+
console.log(` → Appended to ${this.curBlock.type_str}(${this.curBlock.start}-${this.curBlock.end}), now has ${this.curBlock.nodes.length} nodes`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// 3.12 exception-group prolog sometimes leaves synthetic None; drop only if it's a synthetic None before PUSH_EXC_INFO.
|
|
838
|
+
let value = this.dataStack.pop();
|
|
839
|
+
const nextOp = this.code.Next;
|
|
840
|
+
// Only drop return if it's a synthetic None followed by PUSH_EXC_INFO (exception-group prolog pattern).
|
|
841
|
+
// Real returns before exception handlers (like in with-statements) should NOT be dropped.
|
|
842
|
+
if (value instanceof AST.ASTNone && nextOp?.OpCodeID === this.OpCodes.PUSH_EXC_INFO) {
|
|
843
|
+
// Check if this is a synthetic return (no explicit line number or same line as previous instruction)
|
|
844
|
+
const curLine = this.code.Current?.LineNo;
|
|
845
|
+
const prevLine = this.code.Previous?.LineNo;
|
|
846
|
+
if (curLine === prevLine || !curLine) {
|
|
847
|
+
// Likely synthetic None from exception prolog - drop it
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (value == null) {
|
|
852
|
+
value = new AST.ASTNone();
|
|
853
|
+
}
|
|
854
|
+
let node = new AST.ASTReturn(value);
|
|
855
|
+
node.inLambda = this.object.Name == '<lambda>';
|
|
856
|
+
node.line = this.code.Current.LineNo;
|
|
857
|
+
|
|
858
|
+
this.curBlock.append(node);
|
|
859
|
+
|
|
860
|
+
if (this.currentMatch) {
|
|
861
|
+
const hasDefaultAhead = (() => {
|
|
862
|
+
let scan = this.code.Next;
|
|
863
|
+
let steps = 0;
|
|
864
|
+
const retOps = [this.OpCodes.RETURN_CONST_A, this.OpCodes.RETURN_VALUE, this.OpCodes.RETURN_VALUE_A];
|
|
865
|
+
const skipOps = [this.OpCodes.CACHE, this.OpCodes.NOP];
|
|
866
|
+
while (scan && steps < 4) {
|
|
867
|
+
if (retOps.includes(scan.OpCodeID)) return true;
|
|
868
|
+
if (skipOps.includes(scan.OpCodeID)) { scan = scan.Next; steps++; continue; }
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
return false;
|
|
872
|
+
})();
|
|
873
|
+
if (hasDefaultAhead) {
|
|
874
|
+
// Default case ahead - flush current case before default starts
|
|
875
|
+
if (this.currentCase) {
|
|
876
|
+
flushCurrentCaseBody.call(this);
|
|
877
|
+
}
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (!hasUpcomingMatchCase.call(this)) {
|
|
881
|
+
finalizeMatchCase.call(this);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
// More cases coming - flush current case before next one starts
|
|
885
|
+
if (this.currentCase) {
|
|
886
|
+
flushCurrentCaseBody.call(this);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (!this.currentCase && [AST.ASTBlock.BlockType.If, AST.ASTBlock.BlockType.Else].includes(this.curBlock.blockType)
|
|
891
|
+
&& (this.object.Reader.versionCompare(2, 6) >= 0)) {
|
|
892
|
+
let prev = this.curBlock;
|
|
893
|
+
this.blocks.pop();
|
|
894
|
+
this.curBlock = this.blocks.top();
|
|
895
|
+
if (
|
|
896
|
+
prev instanceof AST.ASTCondBlock &&
|
|
897
|
+
prev.nodes.length == 1 &&
|
|
898
|
+
prev.line == value.line
|
|
899
|
+
) {
|
|
900
|
+
prev = new AST.ASTReturn(new AST.ASTBinary(prev.condition, value, prev.negative ? AST.ASTBinary.BinOp.LogicalOr : AST.ASTBinary.BinOp.LogicalAnd));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
this.curBlock.append(prev);
|
|
904
|
+
|
|
905
|
+
if ([this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A].includes(this.code.Next?.OpCodeID)) {
|
|
906
|
+
this.code.GoNext();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function handleInstrumentedReturnConstA() {
|
|
912
|
+
this.handleReturnConstA();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function handleReturnConstA() {
|
|
916
|
+
// If a new pattern matched but case body not started yet, open it now
|
|
917
|
+
// Only start new case if no current case AND (not in pattern OR has unflushed pattern ops)
|
|
918
|
+
if (this.currentMatch) {
|
|
919
|
+
const hasUnflushedPattern = (this.patternOps?.length || 0) > 0;
|
|
920
|
+
const needsNewCase = !this.currentCase && (!this.inMatchPattern || hasUnflushedPattern);
|
|
921
|
+
if (needsNewCase) {
|
|
922
|
+
beginMatchCaseFromPattern.call(this, {reason: 'return'});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const nextOp = this.code.Next;
|
|
927
|
+
if (nextOp?.OpCodeID === this.OpCodes.PUSH_EXC_INFO) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Skip implicit return None in various contexts:
|
|
932
|
+
// 1. Module-level implicit return at end
|
|
933
|
+
// 2. Exception cleanup after successful except* handling (POP_EXCEPT → RETURN_CONST None → SWAP)
|
|
934
|
+
const constObj = this.code.Current.ConstantObject;
|
|
935
|
+
const isNone = constObj?.ClassName === 'Py_None' || constObj === null || constObj === undefined;
|
|
936
|
+
const isModuleLevel = this.object.Name === '<module>';
|
|
937
|
+
const isAtEnd = !nextOp || nextOp.OpCodeID === this.OpCodes.RERAISE_A ||
|
|
938
|
+
nextOp.OpCodeID === this.OpCodes.RERAISE;
|
|
939
|
+
const prevIsPOPExcept = this.code.Prev?.OpCodeID === this.OpCodes.POP_EXCEPT;
|
|
940
|
+
const nextIsSWAP = nextOp?.OpCodeID === this.OpCodes.SWAP_A;
|
|
941
|
+
const isExceptCleanup = isNone && prevIsPOPExcept && nextIsSWAP;
|
|
942
|
+
|
|
943
|
+
if (isNone && isModuleLevel && isAtEnd) {
|
|
944
|
+
return; // Skip implicit return None at module end
|
|
945
|
+
}
|
|
946
|
+
if (isExceptCleanup) {
|
|
947
|
+
return; // Skip return None in exception cleanup (except* success path)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
let value = new AST.ASTObject(this.code.Current.ConstantObject);
|
|
951
|
+
let node = new AST.ASTReturn(value);
|
|
952
|
+
node.line = this.code.Current.LineNo;
|
|
953
|
+
|
|
954
|
+
this.curBlock.append(node);
|
|
955
|
+
|
|
956
|
+
if (this.currentMatch) {
|
|
957
|
+
const hasDefaultAhead = (() => {
|
|
958
|
+
let scan = this.code.Next;
|
|
959
|
+
let steps = 0;
|
|
960
|
+
const retOps = [this.OpCodes.RETURN_CONST_A, this.OpCodes.RETURN_VALUE, this.OpCodes.RETURN_VALUE_A];
|
|
961
|
+
const skipOps = [this.OpCodes.CACHE, this.OpCodes.NOP];
|
|
962
|
+
while (scan && steps < 4) {
|
|
963
|
+
if (retOps.includes(scan.OpCodeID)) return true;
|
|
964
|
+
if (skipOps.includes(scan.OpCodeID)) { scan = scan.Next; steps++; continue; }
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
return false;
|
|
968
|
+
})();
|
|
969
|
+
if (hasDefaultAhead) {
|
|
970
|
+
// Default case ahead - flush current case before default starts
|
|
971
|
+
if (this.currentCase) {
|
|
972
|
+
flushCurrentCaseBody.call(this);
|
|
973
|
+
}
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (!hasUpcomingMatchCase.call(this)) {
|
|
977
|
+
finalizeMatchCase.call(this);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
// More cases coming - flush current case before next one starts
|
|
981
|
+
if (this.currentCase) {
|
|
982
|
+
flushCurrentCaseBody.call(this);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function handleSetLinenoA() {}
|
|
989
|
+
|
|
990
|
+
function handleSetupAnnotations() {
|
|
991
|
+
this.variable_annotations = true;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function handleEndAsyncFor() {
|
|
995
|
+
// Python 3.9+ END_ASYNC_FOR opcode
|
|
996
|
+
// Python 3.9-3.10: finally handler for SETUP_FINALLY in async for loops
|
|
997
|
+
// Python 3.11+: direct exception handler (no CONTAINER/finally wrapper)
|
|
998
|
+
|
|
999
|
+
if (global.g_cliArgs?.debug) {
|
|
1000
|
+
console.log(`[END_ASYNC_FOR] curBlock=${this.curBlock.type_str}, stack depth=${this.blocks.length}`);
|
|
1001
|
+
console.log(` Block stack: ${this.blocks.map((b,i) => `[${i}]${b.type_str}`).join(' → ')}`);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Python 3.11+: Direct async for block (no CONTAINER)
|
|
1005
|
+
if (this.object.Reader.versionCompare(3, 11) >= 0 &&
|
|
1006
|
+
this.curBlock.blockType == AST.ASTBlock.BlockType.AsyncFor) {
|
|
1007
|
+
|
|
1008
|
+
if (global.g_cliArgs?.debug) {
|
|
1009
|
+
console.log(` Python 3.11+ direct AsyncFor, nodes: ${this.curBlock.nodes.length}`);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// If body is empty, add pass
|
|
1013
|
+
if (this.curBlock.nodes.length === 0) {
|
|
1014
|
+
let passNode = new AST.ASTKeyword(AST.ASTKeyword.Word.Pass);
|
|
1015
|
+
passNode.line = this.code.Current.LineNo;
|
|
1016
|
+
this.curBlock.nodes.push(passNode);
|
|
1017
|
+
|
|
1018
|
+
if (global.g_cliArgs?.debug) {
|
|
1019
|
+
console.log(` ✓ Added pass statement to empty Python 3.11 async for body`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Close the async for block
|
|
1024
|
+
this.blocks.pop();
|
|
1025
|
+
this.blocks.top().append(this.curBlock);
|
|
1026
|
+
this.curBlock = this.blocks.top();
|
|
1027
|
+
|
|
1028
|
+
if (global.g_cliArgs?.debug) {
|
|
1029
|
+
console.log(` ✓ Closed Python 3.11 AsyncFor block`);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Python 3.9-3.10: CONTAINER/finally pattern
|
|
1036
|
+
if (this.curBlock.blockType == AST.ASTBlock.BlockType.Finally) {
|
|
1037
|
+
let finallyBlock = this.curBlock;
|
|
1038
|
+
|
|
1039
|
+
// Pop the finally block
|
|
1040
|
+
this.blocks.pop();
|
|
1041
|
+
this.curBlock = this.blocks.top();
|
|
1042
|
+
|
|
1043
|
+
// Check if we're in CONTAINER → AsyncFor pattern
|
|
1044
|
+
if (this.curBlock.blockType == AST.ASTBlock.BlockType.Container &&
|
|
1045
|
+
this.blocks.length > 1) {
|
|
1046
|
+
let cont = this.curBlock;
|
|
1047
|
+
let parentBlock = this.blocks[this.blocks.length - 2];
|
|
1048
|
+
|
|
1049
|
+
if (parentBlock.blockType == AST.ASTBlock.BlockType.AsyncFor &&
|
|
1050
|
+
parentBlock.inited) {
|
|
1051
|
+
|
|
1052
|
+
if (global.g_cliArgs?.debug) {
|
|
1053
|
+
console.log(` Found CONTAINER→AsyncFor(inited), hiding CONTAINER`);
|
|
1054
|
+
console.log(` AsyncFor has ${parentBlock.nodes.length} nodes, Finally has ${finallyBlock.nodes.length} nodes`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Extract loop body from finally block (Python 3.9 uses finally, not try)
|
|
1058
|
+
// Skip STORE (index already set by processStore) and skip async implementation details
|
|
1059
|
+
for (let i = 0; i < finallyBlock.nodes.length; i++) {
|
|
1060
|
+
let node = finallyBlock.nodes[i];
|
|
1061
|
+
// Skip implementation details: STORE, yield from, await calls
|
|
1062
|
+
if (node &&
|
|
1063
|
+
!(node instanceof AST.ASTStore) &&
|
|
1064
|
+
node.constructor.name !== 'ASTReturn' &&
|
|
1065
|
+
!(node.constructor.name === 'ASTCall' && node.func?.name === 'await')) {
|
|
1066
|
+
parentBlock.nodes.push(node);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// If body is empty, add pass
|
|
1071
|
+
if (parentBlock.nodes.length === 0) {
|
|
1072
|
+
let passNode = new AST.ASTKeyword(AST.ASTKeyword.Word.Pass);
|
|
1073
|
+
passNode.line = finallyBlock.line;
|
|
1074
|
+
parentBlock.nodes.push(passNode);
|
|
1075
|
+
|
|
1076
|
+
if (global.g_cliArgs?.debug) {
|
|
1077
|
+
console.log(` ✓ Added pass statement to empty async for body`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Pop CONTAINER without appending it
|
|
1082
|
+
this.blocks.pop();
|
|
1083
|
+
this.curBlock = this.blocks.top();
|
|
1084
|
+
|
|
1085
|
+
// Close AsyncFor early and mark rest as unreachable
|
|
1086
|
+
if (this.curBlock.blockType == AST.ASTBlock.BlockType.AsyncFor) {
|
|
1087
|
+
let asyncForEnd = this.curBlock.end;
|
|
1088
|
+
this.blocks.pop();
|
|
1089
|
+
this.blocks.top().append(this.curBlock);
|
|
1090
|
+
this.curBlock = this.blocks.top();
|
|
1091
|
+
|
|
1092
|
+
// Mark everything until the original AsyncFor end as unreachable
|
|
1093
|
+
this.unreachableUntil = asyncForEnd;
|
|
1094
|
+
|
|
1095
|
+
if (global.g_cliArgs?.debug) {
|
|
1096
|
+
console.log(` ✓ Closed AsyncFor, marking ${this.code.Current.Offset}-${asyncForEnd} as unreachable`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function handleInstrumentedEndAsyncForA() {
|
|
1106
|
+
// Instrumented variant mirrors END_ASYNC_FOR behavior.
|
|
1107
|
+
handleEndAsyncFor.call(this);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function handleInstrumentedInstructionA() {
|
|
1111
|
+
// Instrumentation hook; no AST impact.
|
|
1112
|
+
if (global.g_cliArgs?.debug) {
|
|
1113
|
+
console.log(`[INSTRUMENTED_INSTRUCTION] at offset ${this.code.Current.Offset}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function handleInstrumentedLineA() {
|
|
1118
|
+
// Line profiling hook; skip for decompilation.
|
|
1119
|
+
if (global.g_cliArgs?.debug) {
|
|
1120
|
+
console.log(`[INSTRUMENTED_LINE] at offset ${this.code.Current.Offset}`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
module.exports = {
|
|
1125
|
+
beginMatchCaseFromPattern,
|
|
1126
|
+
flushCurrentCaseBody,
|
|
1127
|
+
handleExecStmt,
|
|
1128
|
+
handleFormatValueA,
|
|
1129
|
+
handleFormatSimple,
|
|
1130
|
+
handleFormatWithSpec,
|
|
1131
|
+
handlePopTop,
|
|
1132
|
+
handlePrintExpr,
|
|
1133
|
+
handlePrintItem,
|
|
1134
|
+
handlePrintItemTo,
|
|
1135
|
+
handlePrintNewline,
|
|
1136
|
+
handlePrintNewlineTo,
|
|
1137
|
+
handleReturnGenerator,
|
|
1138
|
+
handleInstrumentedReturnValueA,
|
|
1139
|
+
handleReturnValue,
|
|
1140
|
+
handleInstrumentedReturnConstA,
|
|
1141
|
+
handleReturnConstA,
|
|
1142
|
+
handleSetLinenoA,
|
|
1143
|
+
handleSetupAnnotations,
|
|
1144
|
+
handleEndAsyncFor,
|
|
1145
|
+
handleInstrumentedEndAsyncForA,
|
|
1146
|
+
handleInstrumentedInstructionA,
|
|
1147
|
+
handleInstrumentedLineA
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
function extractGuardFromBody(nodes) {
|
|
1151
|
+
if (!nodes || nodes.length === 0) {
|
|
1152
|
+
return {guard: null, bodyNodes: []};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (global.g_cliArgs?.debug) {
|
|
1156
|
+
const raw = nodes.map(n => `${n?.constructor?.name || typeof n}:${n?.codeFragment ? n.codeFragment() : ''}`);
|
|
1157
|
+
console.log(`[MATCH] Raw case nodes: ${raw.join(' | ')}`);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
let guard = null;
|
|
1161
|
+
let working = nodes.slice();
|
|
1162
|
+
const prefix = [];
|
|
1163
|
+
while (working.length > 0 && working[0] instanceof AST.ASTStore) {
|
|
1164
|
+
prefix.push(working.shift());
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (working.length === 1 && working[0] instanceof AST.ASTCondBlock &&
|
|
1168
|
+
working[0].blockType == AST.ASTBlock.BlockType.If && !working[0].negative &&
|
|
1169
|
+
!(working[0].nextSibling instanceof AST.ASTBlock)) {
|
|
1170
|
+
guard = working[0].condition;
|
|
1171
|
+
working = prefix.concat(working[0].nodes || []);
|
|
1172
|
+
} else {
|
|
1173
|
+
working = nodes.slice();
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
return {guard, bodyNodes: working};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function guardFromPatternRemainder(remainderOps) {
|
|
1180
|
+
if (!remainderOps || remainderOps.length === 0) {
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const compareOps = remainderOps.filter(op => op.type === 'COMPARE');
|
|
1185
|
+
if (compareOps.length === 0) {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
let guardExpr = null;
|
|
1190
|
+
|
|
1191
|
+
for (const comp of compareOps) {
|
|
1192
|
+
const compareNode = new AST.ASTCompare(comp.left, comp.right, comp.op);
|
|
1193
|
+
if (guardExpr) {
|
|
1194
|
+
guardExpr = new AST.ASTBinary(guardExpr, compareNode, AST.ASTBinary.BinOp.LogicalAnd);
|
|
1195
|
+
} else {
|
|
1196
|
+
guardExpr = compareNode;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return guardExpr;
|
|
1201
|
+
}
|