depyo 1.0.1 → 1.0.3
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/README.md +4 -0
- package/depyo.js +103 -2
- package/lib/OpCodes.js +22 -5
- package/lib/PycDecompiler.js +402 -39
- package/lib/PycDisassembler.js +1 -1
- package/lib/PycReader.js +51 -4
- package/lib/PythonObject.js +40 -6
- package/lib/ast/ast_node.js +292 -71
- package/lib/bytecode/python_3_0.js +1 -1
- package/lib/bytecode/python_3_12.js +1 -1
- package/lib/bytecode/python_3_13.js +13 -13
- package/lib/bytecode/python_3_14.js +13 -13
- package/lib/bytecode/python_3_15.js +183 -0
- package/lib/code_reader.js +107 -146
- package/lib/handlers/collections_update.js +50 -1
- package/lib/handlers/context_managers.js +202 -13
- package/lib/handlers/control_flow_jumps.js +516 -23
- package/lib/handlers/exceptions_blocks.js +85 -22
- package/lib/handlers/formatting.js +60 -17
- package/lib/handlers/function_calls.js +454 -57
- package/lib/handlers/function_class_build.js +159 -64
- package/lib/handlers/generators_async.js +67 -0
- package/lib/handlers/load_store_names.js +190 -57
- package/lib/handlers/loop_iterator.js +162 -6
- package/lib/handlers/misc_other.js +216 -43
- package/lib/handlers/stack_ops.js +81 -19
- package/lib/handlers/subscript_slice.js +103 -1
- package/lib/handlers/unpack.js +18 -16
- package/package.json +1 -1
|
@@ -23,6 +23,179 @@ function findBackwardJump(code, startIndex, currentOffset, backJumpOpcodes) {
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Detect a Py2.x (or early-Py3) boolean expression chain rooted at the current
|
|
27
|
+
// JUMP_IF_* instruction. An expression chain converges at a value consumer
|
|
28
|
+
// (STORE_*, RETURN_*, COMPARE_OP, BINARY_*, UNARY_*, CALL_*, POP_JUMP_IF_*,
|
|
29
|
+
// another JUMP_IF_* for nested chains) without crossing block-level opcodes.
|
|
30
|
+
// This distinguishes `a = b and c or d` (expression, consumer=STORE_NAME) from
|
|
31
|
+
// `if b: pass; if c: pass` (control flow, two separate If blocks).
|
|
32
|
+
function isBoolExprChain(ctx) {
|
|
33
|
+
const OpCodes = ctx.OpCodes;
|
|
34
|
+
const chainJumpOps = new Set([
|
|
35
|
+
OpCodes.JUMP_IF_FALSE_A,
|
|
36
|
+
OpCodes.JUMP_IF_TRUE_A,
|
|
37
|
+
OpCodes.JUMP_IF_FALSE_OR_POP_A,
|
|
38
|
+
OpCodes.JUMP_IF_TRUE_OR_POP_A
|
|
39
|
+
].filter(v => v !== undefined));
|
|
40
|
+
const valueOps = new Set([
|
|
41
|
+
OpCodes.LOAD_NAME_A, OpCodes.LOAD_FAST_A, OpCodes.LOAD_CONST_A,
|
|
42
|
+
OpCodes.LOAD_GLOBAL_A, OpCodes.LOAD_ATTR_A, OpCodes.LOAD_DEREF_A,
|
|
43
|
+
OpCodes.LOAD_CLASSDEREF_A, OpCodes.LOAD_CLOSURE_A,
|
|
44
|
+
OpCodes.LOAD_FAST_CHECK_A, OpCodes.LOAD_FAST_AND_CLEAR_A,
|
|
45
|
+
OpCodes.LOAD_FAST_LOAD_FAST_A,
|
|
46
|
+
OpCodes.LOAD_LOCALS, OpCodes.LOAD_BUILD_CLASS,
|
|
47
|
+
OpCodes.POP_TOP, OpCodes.POP_TOP_A,
|
|
48
|
+
OpCodes.DUP_TOP, OpCodes.DUP_TOP_A, OpCodes.ROT_TWO, OpCodes.ROT_THREE, OpCodes.ROT_FOUR,
|
|
49
|
+
OpCodes.COPY_A, OpCodes.SWAP_A,
|
|
50
|
+
OpCodes.COMPARE_OP, OpCodes.COMPARE_OP_A,
|
|
51
|
+
OpCodes.UNARY_POSITIVE, OpCodes.UNARY_NEGATIVE, OpCodes.UNARY_NOT, OpCodes.UNARY_INVERT, OpCodes.UNARY_CONVERT,
|
|
52
|
+
OpCodes.BINARY_ADD, OpCodes.BINARY_SUBTRACT, OpCodes.BINARY_MULTIPLY, OpCodes.BINARY_DIVIDE,
|
|
53
|
+
OpCodes.BINARY_TRUE_DIVIDE, OpCodes.BINARY_FLOOR_DIVIDE, OpCodes.BINARY_MODULO, OpCodes.BINARY_POWER,
|
|
54
|
+
OpCodes.BINARY_LSHIFT, OpCodes.BINARY_RSHIFT, OpCodes.BINARY_AND, OpCodes.BINARY_OR, OpCodes.BINARY_XOR,
|
|
55
|
+
OpCodes.BINARY_SUBSCR, OpCodes.BINARY_SUBSCR_A,
|
|
56
|
+
OpCodes.BINARY_OP_A, OpCodes.BINARY_OP,
|
|
57
|
+
OpCodes.BINARY_MATRIX_MULTIPLY,
|
|
58
|
+
OpCodes.IS_OP_A, OpCodes.CONTAINS_OP_A
|
|
59
|
+
].filter(v => v !== undefined));
|
|
60
|
+
const consumerOps = new Set([
|
|
61
|
+
OpCodes.STORE_NAME_A, OpCodes.STORE_FAST_A, OpCodes.STORE_GLOBAL_A,
|
|
62
|
+
OpCodes.STORE_DEREF_A, OpCodes.STORE_ATTR_A,
|
|
63
|
+
OpCodes.STORE_SUBSCR, OpCodes.STORE_SUBSCR_A,
|
|
64
|
+
OpCodes.RETURN_VALUE, OpCodes.RETURN_VALUE_A, OpCodes.RETURN_CONST_A,
|
|
65
|
+
OpCodes.POP_JUMP_IF_FALSE_A, OpCodes.POP_JUMP_IF_TRUE_A,
|
|
66
|
+
OpCodes.POP_JUMP_FORWARD_IF_FALSE_A, OpCodes.POP_JUMP_FORWARD_IF_TRUE_A,
|
|
67
|
+
OpCodes.POP_JUMP_FORWARD_IF_NONE_A, OpCodes.POP_JUMP_FORWARD_IF_NOT_NONE_A,
|
|
68
|
+
OpCodes.POP_JUMP_BACKWARD_IF_NONE_A, OpCodes.POP_JUMP_BACKWARD_IF_NOT_NONE_A,
|
|
69
|
+
OpCodes.POP_JUMP_IF_NONE_A, OpCodes.POP_JUMP_IF_NOT_NONE_A,
|
|
70
|
+
OpCodes.CALL_FUNCTION_A, OpCodes.CALL_FUNCTION_KW_A, OpCodes.CALL_FUNCTION_EX_A,
|
|
71
|
+
OpCodes.CALL_A, OpCodes.CALL_KW_A, OpCodes.CALL_METHOD_A,
|
|
72
|
+
OpCodes.PRINT_ITEM, OpCodes.PRINT_ITEM_TO, OpCodes.PRINT_EXPR
|
|
73
|
+
].filter(v => v !== undefined));
|
|
74
|
+
|
|
75
|
+
const startIdx = ctx.code.CurrentInstructionIndex;
|
|
76
|
+
const instructions = ctx.code.Instructions || [];
|
|
77
|
+
if (!instructions[startIdx] || !chainJumpOps.has(instructions[startIdx].OpCodeID)) return false;
|
|
78
|
+
|
|
79
|
+
let maxTarget = instructions[startIdx].JumpTarget;
|
|
80
|
+
if (!maxTarget || maxTarget <= instructions[startIdx].Offset) return false;
|
|
81
|
+
|
|
82
|
+
const maxLookahead = 64;
|
|
83
|
+
for (let i = startIdx + 1; i < instructions.length && i - startIdx <= maxLookahead; i++) {
|
|
84
|
+
const instr = instructions[i];
|
|
85
|
+
if (!instr) return false;
|
|
86
|
+
// Reached convergence offset — inspect what's there
|
|
87
|
+
if (instr.Offset >= maxTarget) {
|
|
88
|
+
// Another JUMP_IF_* at the convergence → nested chain continuation
|
|
89
|
+
if (chainJumpOps.has(instr.OpCodeID)) return true;
|
|
90
|
+
if (consumerOps.has(instr.OpCodeID)) return true;
|
|
91
|
+
// POP_TOP at convergence = control flow cleanup (if-body), not expression.
|
|
92
|
+
// Excluding this keeps `if X and Y: pass` — which always ends at POP_TOP —
|
|
93
|
+
// on the existing if-merge path and lets frame synthesis fire only when
|
|
94
|
+
// the chain value is actually consumed (STORE_*, RETURN_*, CALL_*, etc.).
|
|
95
|
+
if (instr.OpCodeID === OpCodes.POP_TOP || instr.OpCodeID === OpCodes.POP_TOP_A) return false;
|
|
96
|
+
// Any value op at the convergence means expression continues
|
|
97
|
+
// (e.g. chain is sub-expression of a larger one)
|
|
98
|
+
if (valueOps.has(instr.OpCodeID)) return true;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
// Extend the chain by further JUMP_IF_* targets
|
|
102
|
+
if (chainJumpOps.has(instr.OpCodeID)) {
|
|
103
|
+
if (instr.JumpTarget > maxTarget) {
|
|
104
|
+
maxTarget = instr.JumpTarget;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Only value-level opcodes allowed inside the chain window
|
|
109
|
+
if (!valueOps.has(instr.OpCodeID)) return false;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Frame-based reconstruction of Py2.x (and early-Py3) boolean expression chains.
|
|
115
|
+
// A chain of `LOAD + JUMP_IF_FALSE/TRUE + POP_TOP` pairs converging at a value
|
|
116
|
+
// consumer (STORE/RETURN/CALL/...) is the bytecode shape of `a = b and c or d`
|
|
117
|
+
// style expressions. The `isBoolExprChain` detector gates this; here we grow a
|
|
118
|
+
// per-decompiler stack of frames {target, op, lhs} and fold them into a
|
|
119
|
+
// compound AST.ASTBinary when execution reaches each frame's target offset.
|
|
120
|
+
//
|
|
121
|
+
// JUMP_IF_FALSE + POP_TOP => AND (lhs short-circuits when falsy).
|
|
122
|
+
// JUMP_IF_TRUE + POP_TOP => OR (lhs short-circuits when truthy).
|
|
123
|
+
function resolveBoolChainFrames(ctx, atOffset) {
|
|
124
|
+
if (!ctx.boolChainStack || ctx.boolChainStack.length === 0) return;
|
|
125
|
+
while (ctx.boolChainStack.length > 0) {
|
|
126
|
+
const frame = ctx.boolChainStack[ctx.boolChainStack.length - 1];
|
|
127
|
+
if (frame.target > atOffset) break;
|
|
128
|
+
ctx.boolChainStack.pop();
|
|
129
|
+
if (ctx.dataStack.length === 0) {
|
|
130
|
+
// Stack underflow means we misread the chain; drop remaining frames
|
|
131
|
+
// so we don't synthesize garbage and let normal handling resume.
|
|
132
|
+
ctx.boolChainStack = [];
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const rhs = ctx.dataStack.pop();
|
|
136
|
+
const op = frame.op === 'AND'
|
|
137
|
+
? AST.ASTBinary.BinOp.LogicalAnd
|
|
138
|
+
: AST.ASTBinary.BinOp.LogicalOr;
|
|
139
|
+
const combined = new AST.ASTBinary(frame.lhs, rhs, op);
|
|
140
|
+
combined.line = ctx.code.Current?.LineNo ?? 0;
|
|
141
|
+
ctx.dataStack.push(combined);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function tryStartBoolChain(ctx) {
|
|
146
|
+
const OpCodes = ctx.OpCodes;
|
|
147
|
+
// Only Py2.x peek-and-branch (JUMP_IF_FALSE_A / JUMP_IF_TRUE_A + POP_TOP).
|
|
148
|
+
// The _OR_POP variants are already reconstructed correctly by the
|
|
149
|
+
// existing 3.x handler; intercepting them here would double-wrap.
|
|
150
|
+
const peekJumps = [OpCodes.JUMP_IF_FALSE_A, OpCodes.JUMP_IF_TRUE_A]
|
|
151
|
+
.filter(v => v !== undefined);
|
|
152
|
+
if (!peekJumps.includes(ctx.code.Current.OpCodeID)) return false;
|
|
153
|
+
if (ctx.inMatchPattern) return false;
|
|
154
|
+
if (ctx.dataStack.length === 0) return false;
|
|
155
|
+
if (!isBoolExprChain(ctx)) return false;
|
|
156
|
+
|
|
157
|
+
const rawTarget = ctx.code.Current.JumpTarget;
|
|
158
|
+
if (!rawTarget || rawTarget <= ctx.code.Current.Offset) return false;
|
|
159
|
+
|
|
160
|
+
const isFalseJump = ctx.code.Current.OpCodeID === OpCodes.JUMP_IF_FALSE_A;
|
|
161
|
+
|
|
162
|
+
// Py2.4+ optimization: the outer JUMP_IF_* may target a shared POP_TOP
|
|
163
|
+
// that also serves as the inner chain's fall-through sink (merged
|
|
164
|
+
// false-branch entry). In that case raw target points past an inner
|
|
165
|
+
// JUMP_IF_*, which would nest the inner chain under the outer frame —
|
|
166
|
+
// wrong parse. Re-aim the frame at the inner JUMP_IF_* offset so the
|
|
167
|
+
// outer resolves BEFORE the inner pushes its own frame.
|
|
168
|
+
// Shape: <outer JUMP_IF> ... POP_TOP <LOADs>* <inner JUMP_IF> POP_TOP@rawTarget <LOADs>* <consumer>.
|
|
169
|
+
let target = rawTarget;
|
|
170
|
+
const instructions = ctx.code.Instructions || [];
|
|
171
|
+
const curIdx = ctx.code.CurrentInstructionIndex;
|
|
172
|
+
const chainJumpOps = new Set([
|
|
173
|
+
OpCodes.JUMP_IF_FALSE_A, OpCodes.JUMP_IF_TRUE_A
|
|
174
|
+
].filter(v => v !== undefined));
|
|
175
|
+
const popOps = new Set([OpCodes.POP_TOP, OpCodes.POP_TOP_A].filter(v => v !== undefined));
|
|
176
|
+
const rawTargetIdx = instructions.findIndex(i => i && i.Offset === rawTarget);
|
|
177
|
+
if (rawTargetIdx > curIdx + 1 &&
|
|
178
|
+
popOps.has(instructions[rawTargetIdx]?.OpCodeID)) {
|
|
179
|
+
for (let i = rawTargetIdx - 1; i > curIdx; i--) {
|
|
180
|
+
const instr = instructions[i];
|
|
181
|
+
if (!instr) break;
|
|
182
|
+
if (chainJumpOps.has(instr.OpCodeID) && instr.JumpTarget >= rawTarget) {
|
|
183
|
+
target = instr.Offset;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const lhs = ctx.dataStack.pop();
|
|
190
|
+
ctx.boolChainStack = ctx.boolChainStack || [];
|
|
191
|
+
ctx.boolChainStack.push({ target, op: isFalseJump ? 'AND' : 'OR', lhs });
|
|
192
|
+
|
|
193
|
+
if (ctx.code.Next?.OpCodeID === OpCodes.POP_TOP) {
|
|
194
|
+
ctx.code.GoNext();
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
26
199
|
function tryStartConditionalExpression(ctx, cond, falseOffset) {
|
|
27
200
|
if (!cond || falseOffset <= ctx.code.Current.Offset) {
|
|
28
201
|
return false;
|
|
@@ -32,13 +205,53 @@ function tryStartConditionalExpression(ctx, cond, falseOffset) {
|
|
|
32
205
|
return false;
|
|
33
206
|
}
|
|
34
207
|
|
|
208
|
+
// Statement terminators inside the true-branch: if the branch ends with a
|
|
209
|
+
// side-effecting instruction (STORE_*, POP_TOP, RETURN, RAISE) before the
|
|
210
|
+
// candidate JUMP_FORWARD, this is an if/else, not a ternary, and we must
|
|
211
|
+
// not push a pending — that would suppress the real If block.
|
|
212
|
+
const storeOps = new Set([
|
|
213
|
+
ctx.OpCodes.STORE_FAST_A,
|
|
214
|
+
ctx.OpCodes.STORE_NAME_A,
|
|
215
|
+
ctx.OpCodes.STORE_GLOBAL_A,
|
|
216
|
+
ctx.OpCodes.STORE_DEREF_A,
|
|
217
|
+
ctx.OpCodes.STORE_ATTR_A,
|
|
218
|
+
ctx.OpCodes.STORE_SUBSCR,
|
|
219
|
+
ctx.OpCodes.STORE_SUBSCR_A,
|
|
220
|
+
ctx.OpCodes.POP_TOP,
|
|
221
|
+
ctx.OpCodes.POP_TOP_A,
|
|
222
|
+
ctx.OpCodes.RETURN_VALUE,
|
|
223
|
+
ctx.OpCodes.RETURN_VALUE_A,
|
|
224
|
+
ctx.OpCodes.RETURN_CONST_A,
|
|
225
|
+
ctx.OpCodes.RAISE_VARARGS_A,
|
|
226
|
+
].filter(v => v !== undefined));
|
|
227
|
+
const blockOpeners = new Set([
|
|
228
|
+
ctx.OpCodes.SETUP_EXCEPT_A,
|
|
229
|
+
ctx.OpCodes.SETUP_FINALLY_A,
|
|
230
|
+
ctx.OpCodes.SETUP_LOOP_A,
|
|
231
|
+
ctx.OpCodes.SETUP_WITH_A,
|
|
232
|
+
ctx.OpCodes.SETUP_ASYNC_WITH_A,
|
|
233
|
+
ctx.OpCodes.FOR_ITER_A,
|
|
234
|
+
].filter(v => v !== undefined));
|
|
235
|
+
|
|
35
236
|
let joinInstr = null;
|
|
36
237
|
for (let i = ctx.code.CurrentInstructionIndex + 1; i < falseIdx; i++) {
|
|
37
238
|
const instr = ctx.code.Instructions[i];
|
|
38
239
|
if (!instr) {
|
|
39
240
|
continue;
|
|
40
241
|
}
|
|
242
|
+
// Crossing a block opener disqualifies: the JUMP_FORWARD inside the
|
|
243
|
+
// nested block is not the true-branch's join.
|
|
244
|
+
if (blockOpeners.has(instr.OpCodeID)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
41
247
|
if ([ctx.OpCodes.JUMP_FORWARD_A, ctx.OpCodes.JUMP_ABSOLUTE_A].includes(instr.OpCodeID)) {
|
|
248
|
+
// Look at the instruction immediately before the jump. For a ternary,
|
|
249
|
+
// it should leave a value on the stack (LOAD_*, BUILD_*, arithmetic),
|
|
250
|
+
// not be a statement terminator.
|
|
251
|
+
const prev = ctx.code.Instructions[i - 1];
|
|
252
|
+
if (prev && storeOps.has(prev.OpCodeID)) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
42
255
|
const target = instr.JumpTarget ?? -1;
|
|
43
256
|
if (target > falseOffset && target <= falseOffset + 50) {
|
|
44
257
|
joinInstr = instr;
|
|
@@ -71,6 +284,33 @@ function captureTrueBranchForConditional(ctx) {
|
|
|
71
284
|
if (!pending) {
|
|
72
285
|
return false;
|
|
73
286
|
}
|
|
287
|
+
// A real ternary leaves the true-branch expression on the data stack; if
|
|
288
|
+
// the stack is empty, this JUMP_FORWARD belongs to some other structure
|
|
289
|
+
// (e.g. a nested try/except whose exit jump happens to land on the same
|
|
290
|
+
// offset) and we must not consume it.
|
|
291
|
+
if (!ctx.dataStack.length) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
// Block openers between pending.startOffset and here mean the JUMP_FORWARD
|
|
295
|
+
// is inside a nested scope and cannot be the ternary's true branch.
|
|
296
|
+
const blockOpeners = new Set([
|
|
297
|
+
ctx.OpCodes.SETUP_EXCEPT_A,
|
|
298
|
+
ctx.OpCodes.SETUP_FINALLY_A,
|
|
299
|
+
ctx.OpCodes.SETUP_LOOP_A,
|
|
300
|
+
ctx.OpCodes.SETUP_WITH_A,
|
|
301
|
+
ctx.OpCodes.SETUP_ASYNC_WITH_A,
|
|
302
|
+
ctx.OpCodes.FOR_ITER_A,
|
|
303
|
+
].filter(v => v !== undefined));
|
|
304
|
+
const startIdx = ctx.code.GetIndexByOffset(pending.startOffset);
|
|
305
|
+
const curIdx = ctx.code.CurrentInstructionIndex;
|
|
306
|
+
if (startIdx >= 0) {
|
|
307
|
+
for (let i = startIdx + 1; i < curIdx; i++) {
|
|
308
|
+
const instr = ctx.code.Instructions[i];
|
|
309
|
+
if (instr && blockOpeners.has(instr.OpCodeID)) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
74
314
|
pending.trueValue = ctx.dataStack.pop();
|
|
75
315
|
return true;
|
|
76
316
|
}
|
|
@@ -156,6 +396,15 @@ function processJumpOps() {
|
|
|
156
396
|
return;
|
|
157
397
|
}
|
|
158
398
|
|
|
399
|
+
// Intercept boolean expression chains (a = b and c or d style) before the
|
|
400
|
+
// If-block machinery. If this JUMP_IF_* is part of a chain converging at a
|
|
401
|
+
// value consumer, push a frame and let resolveBoolChainFrames fold the
|
|
402
|
+
// combined expression back onto dataStack when the chain's target offset
|
|
403
|
+
// is reached. See comments on tryStartBoolChain / resolveBoolChainFrames.
|
|
404
|
+
if (tryStartBoolChain(this)) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
159
408
|
const wrapNoneComparison = (expectNone) => {
|
|
160
409
|
let value = this.dataStack.pop();
|
|
161
410
|
let cmp = new AST.ASTCompare(
|
|
@@ -213,8 +462,29 @@ function processJumpOps() {
|
|
|
213
462
|
}
|
|
214
463
|
|
|
215
464
|
// CRITICAL: Close blocks that have ended before creating new conditional block
|
|
216
|
-
// This ensures proper sibling relationships between if/elif blocks
|
|
217
|
-
|
|
465
|
+
// This ensures proper sibling relationships between if/elif blocks.
|
|
466
|
+
//
|
|
467
|
+
// Exception: a Py2.x boolean expression chain (`a = b and c or d`) compiles
|
|
468
|
+
// to a sequence of JUMP_IF_FALSE/JUMP_IF_TRUE + POP_TOP pairs converging at
|
|
469
|
+
// a consumer (STORE/RETURN/CALL/etc). The first JUMP_IF_* opens an empty
|
|
470
|
+
// If(end=T) block; when we reach offset T we find another JUMP_IF_* that
|
|
471
|
+
// should continue the chain via the AND/OR merge below (line ~540). If we
|
|
472
|
+
// close here, the merge opportunity is lost and the expression renders as
|
|
473
|
+
// stray `if X: pass` control flow.
|
|
474
|
+
const chainJumpOps = [
|
|
475
|
+
this.OpCodes.JUMP_IF_FALSE_A,
|
|
476
|
+
this.OpCodes.JUMP_IF_TRUE_A,
|
|
477
|
+
this.OpCodes.JUMP_IF_FALSE_OR_POP_A,
|
|
478
|
+
this.OpCodes.JUMP_IF_TRUE_OR_POP_A
|
|
479
|
+
].filter(v => v !== undefined);
|
|
480
|
+
const keepOpenForChain = chainJumpOps.includes(this.code.Current.OpCodeID) &&
|
|
481
|
+
this.curBlock.size === 0 &&
|
|
482
|
+
this.curBlock.end === this.code.Current.Offset &&
|
|
483
|
+
[AST.ASTBlock.BlockType.If, AST.ASTBlock.BlockType.Elif, AST.ASTBlock.BlockType.While].includes(this.curBlock.blockType) &&
|
|
484
|
+
isBoolExprChain(this);
|
|
485
|
+
|
|
486
|
+
while (!keepOpenForChain &&
|
|
487
|
+
this.curBlock.end > 0 &&
|
|
218
488
|
this.curBlock.end <= this.code.Current.Offset &&
|
|
219
489
|
this.curBlock.blockType != AST.ASTBlock.BlockType.Main &&
|
|
220
490
|
this.blocks.length > 1) {
|
|
@@ -339,9 +609,19 @@ function processJumpOps() {
|
|
|
339
609
|
}
|
|
340
610
|
}
|
|
341
611
|
|
|
612
|
+
const isEgMatchCall = cond instanceof AST.ASTCall &&
|
|
613
|
+
cond.func instanceof AST.ASTName &&
|
|
614
|
+
cond.func.name === "__check_eg_match__";
|
|
615
|
+
const isExceptCompare = cond instanceof AST.ASTCompare &&
|
|
616
|
+
cond.op == AST.ASTCompare.CompareOp.Exception;
|
|
617
|
+
|
|
342
618
|
// Conditional expression (a if cond else b) heuristic:
|
|
343
619
|
// POP_JUMP_IF_* to falseOffset, then JUMP_FORWARD to joinOffset > falseOffset.
|
|
620
|
+
// Skip for exception matches: except handlers emit a similar bytecode shape
|
|
621
|
+
// (POP_JUMP_IF_FALSE past handler body, JUMP_FORWARD over END_FINALLY) but
|
|
622
|
+
// are not ternary expressions and must not be rewritten.
|
|
344
623
|
if (!this.inMatchPattern &&
|
|
624
|
+
!isExceptCompare && !isEgMatchCall &&
|
|
345
625
|
condJumpOpcodes.has(this.code.Current.OpCodeID) &&
|
|
346
626
|
tryStartConditionalExpression(this, cond, rawJumpTarget)) {
|
|
347
627
|
return;
|
|
@@ -362,12 +642,6 @@ function processJumpOps() {
|
|
|
362
642
|
}
|
|
363
643
|
}
|
|
364
644
|
|
|
365
|
-
const isEgMatchCall = cond instanceof AST.ASTCall &&
|
|
366
|
-
cond.func instanceof AST.ASTName &&
|
|
367
|
-
cond.func.name === "__check_eg_match__";
|
|
368
|
-
const isExceptCompare = cond instanceof AST.ASTCompare &&
|
|
369
|
-
cond.op == AST.ASTCompare.CompareOp.Exception;
|
|
370
|
-
|
|
371
645
|
if (isExceptCompare || isEgMatchCall) {
|
|
372
646
|
if (global.g_cliArgs?.debug) {
|
|
373
647
|
const matchType = isExceptCompare ? cond.right : cond.pparams?.[1];
|
|
@@ -377,13 +651,15 @@ function processJumpOps() {
|
|
|
377
651
|
const handlerEnd = this.findExceptionHandlerEnd ? this.findExceptionHandlerEnd(this.code.Current.Offset) : null;
|
|
378
652
|
if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except
|
|
379
653
|
&& this.curBlock.condition == null) {
|
|
380
|
-
// Reuse current except block (from exception table
|
|
654
|
+
// Reuse current except block (from exception table or pushed by JUMP_FORWARD)
|
|
655
|
+
// instead of creating a nested one. Return immediately to avoid the
|
|
656
|
+
// fall-through that would otherwise create a stray If block on top.
|
|
381
657
|
this.curBlock.condition = isExceptCompare ? cond.right : cond.pparams?.[1];
|
|
382
658
|
if (handlerEnd) {
|
|
383
659
|
this.curBlock.end = handlerEnd;
|
|
384
660
|
}
|
|
385
661
|
this.curBlock.init();
|
|
386
|
-
|
|
662
|
+
return;
|
|
387
663
|
} else {
|
|
388
664
|
const end = handlerEnd || offs;
|
|
389
665
|
const matchType = isExceptCompare ? cond.right : cond.pparams?.[1];
|
|
@@ -398,10 +674,26 @@ function processJumpOps() {
|
|
|
398
674
|
}
|
|
399
675
|
}
|
|
400
676
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
677
|
+
// Only collapse else→elif when the Else attaches to an If/Elif.
|
|
678
|
+
// `while/for ... else: if X: ...` must stay as a nested if, since
|
|
679
|
+
// rendering `elif` after `while/for` is invalid Python. The Else's
|
|
680
|
+
// prior sibling (last node in the enclosing block) reveals what
|
|
681
|
+
// the Else was created for.
|
|
682
|
+
let elseHolder = this.blocks.length >= 2
|
|
683
|
+
? this.blocks[this.blocks.length - 2]
|
|
684
|
+
: null;
|
|
685
|
+
let priorSibling = elseHolder && elseHolder.size > 0
|
|
686
|
+
? elseHolder.nodes[elseHolder.size - 1]
|
|
687
|
+
: null;
|
|
688
|
+
let elseAttachesToIf = priorSibling instanceof AST.ASTCondBlock &&
|
|
689
|
+
(priorSibling.blockType == AST.ASTBlock.BlockType.If ||
|
|
690
|
+
priorSibling.blockType == AST.ASTBlock.BlockType.Elif);
|
|
691
|
+
|
|
692
|
+
if (elseAttachesToIf &&
|
|
693
|
+
(this.curBlock.size == 0 ||
|
|
694
|
+
(this.curBlock.size == 1 &&
|
|
695
|
+
this.curBlock.nodes[0] instanceof AST.ASTCondBlock &&
|
|
696
|
+
this.curBlock.nodes[0].blockType == AST.ASTBlock.BlockType.If))) {
|
|
405
697
|
/* Collapse into elif statement */
|
|
406
698
|
if (global.g_cliArgs?.debug) {
|
|
407
699
|
console.log(`ELIF DETECTED: else block size=${this.curBlock.size}, converting to elif at offset ${this.code.Current.Offset}`);
|
|
@@ -462,7 +754,12 @@ function processJumpOps() {
|
|
|
462
754
|
&& this.curBlock.comprehension
|
|
463
755
|
&& this.object.Reader.versionCompare(2, 7) >= 0) {
|
|
464
756
|
/* Comprehension condition */
|
|
465
|
-
|
|
757
|
+
let actualCond = cond;
|
|
758
|
+
if (neg) {
|
|
759
|
+
actualCond = new AST.ASTUnary(cond, AST.ASTUnary.UnaryOp.Not);
|
|
760
|
+
actualCond.line = this.code.Current.LineNo;
|
|
761
|
+
}
|
|
762
|
+
this.curBlock.condition = actualCond;
|
|
466
763
|
return;
|
|
467
764
|
}
|
|
468
765
|
|
|
@@ -493,10 +790,21 @@ function processJumpOps() {
|
|
|
493
790
|
lastNode.blockType == AST.ASTBlock.BlockType.Elif) &&
|
|
494
791
|
!lastNodeInStack) { // CLOSED, not in stack = sibling!
|
|
495
792
|
|
|
496
|
-
|
|
793
|
+
// Assert-style guards (`if not X: raise`) don't chain as elif.
|
|
794
|
+
// `assert X; assert Y` and `if not X: raise; if not Y: raise`
|
|
795
|
+
// share bytecode shape with elif, but rendering the second as
|
|
796
|
+
// elif creates an orphan `elif` after the first becomes `assert`.
|
|
797
|
+
let priorIsAssertGuard =
|
|
798
|
+
lastNode.negative &&
|
|
799
|
+
lastNode.nodes.length == 1 &&
|
|
800
|
+
lastNode.nodes[0] instanceof AST.ASTRaise;
|
|
497
801
|
|
|
498
|
-
if (
|
|
499
|
-
|
|
802
|
+
if (!priorIsAssertGuard) {
|
|
803
|
+
shouldBeElif = true;
|
|
804
|
+
|
|
805
|
+
if (global.g_cliArgs?.debug) {
|
|
806
|
+
console.log(`ELIF DETECTED: Creating elif at offset ${this.code.Current.Offset} (follows ${lastNode.type_str} at ${lastNode.start})`);
|
|
807
|
+
}
|
|
500
808
|
}
|
|
501
809
|
}
|
|
502
810
|
}
|
|
@@ -552,6 +860,37 @@ function handleJumpAbsoluteA() {
|
|
|
552
860
|
|
|
553
861
|
// [offs] = this.code.FindEndOfBlock(offs);
|
|
554
862
|
|
|
863
|
+
// Inside a try-inside-loop (e.g. `while x: try: ... except: ...`),
|
|
864
|
+
// the successful-path jump back to the loop is emitted as JUMP_ABSOLUTE
|
|
865
|
+
// instead of JUMP_FORWARD. If we don't open the Except block here, the
|
|
866
|
+
// handler body ends up appended to the container as raw statements and
|
|
867
|
+
// the `__exception__` placeholder leaks into the output.
|
|
868
|
+
if (this.curBlock.blockType == AST.ASTBlock.BlockType.Container) {
|
|
869
|
+
let cont = this.curBlock;
|
|
870
|
+
if (cont.hasExcept && this.code.Next?.Offset <= cont.except) {
|
|
871
|
+
// Find the END_FINALLY that closes this except so the handler body
|
|
872
|
+
// stays attached to the Except block rather than leaking back into
|
|
873
|
+
// the Container.
|
|
874
|
+
let handlerEnd = cont.except;
|
|
875
|
+
let cursor = cont.except;
|
|
876
|
+
for (let i = 0; i < 200; i++) {
|
|
877
|
+
const instr = this.code.PeekInstructionAtOffset(cursor);
|
|
878
|
+
if (!instr) break;
|
|
879
|
+
if (instr.OpCodeID === this.OpCodes.END_FINALLY) {
|
|
880
|
+
handlerEnd = instr.Offset;
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
cursor = instr.Offset + (instr.Size || 3);
|
|
884
|
+
}
|
|
885
|
+
cont.end = handlerEnd;
|
|
886
|
+
let except = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, this.code.Current.Offset, handlerEnd, null, false);
|
|
887
|
+
except.init();
|
|
888
|
+
this.blocks.push(except);
|
|
889
|
+
this.curBlock = this.blocks.top();
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
555
894
|
if (offs <= this.code.Next?.Offset) {
|
|
556
895
|
if (this.curBlock.blockType == AST.ASTBlock.BlockType.For) {
|
|
557
896
|
let is_jump_to_start = offs == this.curBlock.start;
|
|
@@ -564,11 +903,63 @@ function handleJumpAbsoluteA() {
|
|
|
564
903
|
let top = this.dataStack.top();
|
|
565
904
|
|
|
566
905
|
if (top instanceof AST.ASTComprehension) {
|
|
567
|
-
|
|
568
|
-
comp.addGenerator(this.curBlock);
|
|
906
|
+
top.addGenerator(this.curBlock);
|
|
569
907
|
this.blocks.pop();
|
|
570
908
|
this.curBlock = this.blocks.top();
|
|
571
|
-
|
|
909
|
+
// For multi-clause comprehensions/genexprs, keep the
|
|
910
|
+
// ASTComprehension on the data stack so the enclosing
|
|
911
|
+
// For+comprehension block can attach its own generator.
|
|
912
|
+
// Only materialize it as a statement once we're clear of
|
|
913
|
+
// comprehension-tagged for-blocks.
|
|
914
|
+
if (!(this.curBlock.blockType == AST.ASTBlock.BlockType.For && this.curBlock.comprehension)) {
|
|
915
|
+
// Detect bare listcomp statement vs assignment/return.
|
|
916
|
+
// Bare form ends with POP_TOP (possibly after DELETE_FAST/
|
|
917
|
+
// DELETE_NAME that drops the cached `_[N]`). If POP_TOP
|
|
918
|
+
// is the terminator, materialize the comprehension as a
|
|
919
|
+
// statement; otherwise leave it on the stack so the
|
|
920
|
+
// downstream consumer (STORE_NAME, RETURN_VALUE, CALL,
|
|
921
|
+
// BINARY_*, …) can use it as an expression.
|
|
922
|
+
// Generator expressions always materialize as the sole
|
|
923
|
+
// body statement so the enclosing CALL handler can
|
|
924
|
+
// extract the comp from the <genexpr> code object.
|
|
925
|
+
let isBareStatement = top.kind === AST.ASTComprehension.GENERATOR;
|
|
926
|
+
if (!isBareStatement && this.code.PeekInstructionAtOffset) {
|
|
927
|
+
let scanOff = this.code.Next?.Offset;
|
|
928
|
+
const SKIP_OPS = [
|
|
929
|
+
this.OpCodes.DELETE_FAST_A,
|
|
930
|
+
this.OpCodes.DELETE_NAME_A,
|
|
931
|
+
this.OpCodes.DELETE_GLOBAL_A,
|
|
932
|
+
this.OpCodes.DELETE_DEREF_A,
|
|
933
|
+
this.OpCodes.JUMP_ABSOLUTE_A,
|
|
934
|
+
this.OpCodes.JUMP_FORWARD_A
|
|
935
|
+
];
|
|
936
|
+
for (let i = 0; i < 12 && scanOff != null; i++) {
|
|
937
|
+
const ins = this.code.PeekInstructionAtOffset(scanOff);
|
|
938
|
+
if (!ins) break;
|
|
939
|
+
if (SKIP_OPS.includes(ins.OpCodeID)) {
|
|
940
|
+
scanOff = ins.Offset + (ins.Size || 3);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (ins.OpCodeID == this.OpCodes.POP_TOP) {
|
|
944
|
+
// Distinguish real bare-listcomp marker
|
|
945
|
+
// from intra-loop skip-path POP_TOP in
|
|
946
|
+
// 2.x (which is always followed by a
|
|
947
|
+
// JUMP_ABSOLUTE back to FOR_ITER).
|
|
948
|
+
const nextIns = this.code.PeekInstructionAtOffset(ins.Offset + (ins.Size || 1));
|
|
949
|
+
if (nextIns && SKIP_OPS.includes(nextIns.OpCodeID)) {
|
|
950
|
+
scanOff = ins.Offset + (ins.Size || 1);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
isBareStatement = true;
|
|
954
|
+
}
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (isBareStatement) {
|
|
959
|
+
let comp = this.dataStack.pop();
|
|
960
|
+
this.curBlock.append(comp);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
572
963
|
} else {
|
|
573
964
|
let tmp = this.curBlock;
|
|
574
965
|
this.blocks.pop();
|
|
@@ -590,6 +981,74 @@ function handleJumpAbsoluteA() {
|
|
|
590
981
|
this.blocks.top().append(this.curBlock);
|
|
591
982
|
this.curBlock = this.blocks.top();
|
|
592
983
|
}
|
|
984
|
+
} else if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except
|
|
985
|
+
&& this.code.PeekInstructionAtOffset
|
|
986
|
+
&& ![this.OpCodes.JUMP_ABSOLUTE_A, this.OpCodes.JUMP_FORWARD_A].includes(this.code.Prev?.OpCodeID)
|
|
987
|
+
&& this.blocks.length >= 2
|
|
988
|
+
&& this.blocks[this.blocks.length - 2].blockType == AST.ASTBlock.BlockType.Container) {
|
|
989
|
+
// Python 2.x try/except/else inside a loop: the except handler
|
|
990
|
+
// body ends with JUMP_ABSOLUTE back to the loop start (skipping
|
|
991
|
+
// the else body). Bytecode then has POP_TOP + END_FINALLY + else
|
|
992
|
+
// body, and the else body itself ends with another JUMP_ABSOLUTE
|
|
993
|
+
// to the same loop start. Without this branch, the else body
|
|
994
|
+
// gets attached to the surrounding loop instead of the try.
|
|
995
|
+
let endFinallyAhead = false;
|
|
996
|
+
let scan = this.code.Next?.Offset;
|
|
997
|
+
for (let i = 0; i < 4 && scan != null; i++) {
|
|
998
|
+
const instr = this.code.PeekInstructionAtOffset(scan);
|
|
999
|
+
if (!instr) break;
|
|
1000
|
+
if (instr.OpCodeID === this.OpCodes.END_FINALLY) {
|
|
1001
|
+
endFinallyAhead = true;
|
|
1002
|
+
scan = instr.Offset + (instr.Size || 1);
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
if (instr.OpCodeID !== this.OpCodes.POP_TOP) break;
|
|
1006
|
+
scan = instr.Offset + (instr.Size || 1);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (endFinallyAhead) {
|
|
1010
|
+
// Distinguish try/except/else from try/except (no else) inside
|
|
1011
|
+
// a loop. With else: bytecode after END_FINALLY is the else
|
|
1012
|
+
// body, then a JUMP_ABSOLUTE to loop start. Without else: the
|
|
1013
|
+
// very next instruction is the loop's exit JUMP_ABSOLUTE.
|
|
1014
|
+
let elseEnd = scan;
|
|
1015
|
+
let cursor = scan;
|
|
1016
|
+
let hasBody = false;
|
|
1017
|
+
for (let i = 0; i < 200; i++) {
|
|
1018
|
+
const instr = this.code.PeekInstructionAtOffset(cursor);
|
|
1019
|
+
if (!instr) break;
|
|
1020
|
+
if (instr.OpCodeID === this.OpCodes.JUMP_ABSOLUTE_A
|
|
1021
|
+
&& instr.JumpTarget === this.code.Current.JumpTarget) {
|
|
1022
|
+
elseEnd = instr.Offset + (instr.Size || 3);
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
if (instr.OpCodeID === this.OpCodes.POP_BLOCK) {
|
|
1026
|
+
elseEnd = instr.Offset;
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
hasBody = true;
|
|
1030
|
+
cursor = instr.Offset + (instr.Size || 3);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (!hasBody) {
|
|
1034
|
+
// No else body — fall through to default jump handling.
|
|
1035
|
+
} else {
|
|
1036
|
+
let except = this.blocks.pop();
|
|
1037
|
+
this.curBlock = this.blocks.top();
|
|
1038
|
+
if (!except.empty()) {
|
|
1039
|
+
this.curBlock.append(except);
|
|
1040
|
+
}
|
|
1041
|
+
if (this.curBlock.blockType == AST.ASTBlock.BlockType.Container) {
|
|
1042
|
+
this.curBlock.end = elseEnd;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
let elseblk = new AST.ASTBlock(AST.ASTBlock.BlockType.Else, this.code.Current.Offset, elseEnd);
|
|
1046
|
+
elseblk.init();
|
|
1047
|
+
this.blocks.push(elseblk);
|
|
1048
|
+
this.curBlock = this.blocks.top();
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
593
1052
|
} else {
|
|
594
1053
|
// First of all we have to figure out if there is any While or For blocks wer are in
|
|
595
1054
|
let loopBlock = null;
|
|
@@ -601,6 +1060,19 @@ function handleJumpAbsoluteA() {
|
|
|
601
1060
|
}
|
|
602
1061
|
|
|
603
1062
|
if (!loopBlock) {
|
|
1063
|
+
// Self-referential JUMP_ABSOLUTE with no enclosing loop is a
|
|
1064
|
+
// `while True: pass` that 3.8+ emits without SETUP_LOOP. Emit a
|
|
1065
|
+
// synthesized while block so the infinite loop is preserved.
|
|
1066
|
+
// Pre-3.8 still has SETUP_LOOP and different jump patterns; leave
|
|
1067
|
+
// those branches to the existing loop machinery.
|
|
1068
|
+
if (offs === this.code.Current.Offset
|
|
1069
|
+
&& this.object.Reader.versionCompare(3, 8) >= 0) {
|
|
1070
|
+
const whileBlk = new AST.ASTCondBlock(AST.ASTBlock.BlockType.While, offs, offs, null, false);
|
|
1071
|
+
whileBlk.init();
|
|
1072
|
+
whileBlk.line = this.code.Current.LineNo;
|
|
1073
|
+
whileBlk.append(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
1074
|
+
this.curBlock.append(whileBlk);
|
|
1075
|
+
}
|
|
604
1076
|
return;
|
|
605
1077
|
}
|
|
606
1078
|
|
|
@@ -825,7 +1297,27 @@ function processJumpForward() {
|
|
|
825
1297
|
}
|
|
826
1298
|
let next = null;
|
|
827
1299
|
|
|
828
|
-
|
|
1300
|
+
// try/except/else: JUMP_FORWARD at end of except handler skips
|
|
1301
|
+
// past END_FINALLY and the else body. Detect by scanning the
|
|
1302
|
+
// few instructions between code.Next and the JUMP target for
|
|
1303
|
+
// an END_FINALLY (Python 2.x emits POP_TOP before END_FINALLY).
|
|
1304
|
+
let jumpTarget = this.code.Next?.Offset + offs;
|
|
1305
|
+
let nextIsElse = this.code.Next?.OpCodeID == this.OpCodes.END_FINALLY;
|
|
1306
|
+
if (!nextIsElse && this.code.PeekInstructionAtOffset) {
|
|
1307
|
+
let scan = this.code.Next?.Offset;
|
|
1308
|
+
for (let i = 0; i < 4 && scan != null && scan < jumpTarget; i++) {
|
|
1309
|
+
const instr = this.code.PeekInstructionAtOffset(scan);
|
|
1310
|
+
if (!instr) break;
|
|
1311
|
+
if (instr.OpCodeID === this.OpCodes.END_FINALLY) {
|
|
1312
|
+
nextIsElse = true;
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
if (instr.OpCodeID !== this.OpCodes.POP_TOP) break;
|
|
1316
|
+
scan = instr.Offset + instr.Size;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (nextIsElse) {
|
|
829
1321
|
next = new AST.ASTBlock(AST.ASTBlock.BlockType.Else, this.code.Current.Offset, this.code.Current.JumpTarget);
|
|
830
1322
|
next.init();
|
|
831
1323
|
} else {
|
|
@@ -950,5 +1442,6 @@ module.exports = {
|
|
|
950
1442
|
handleJumpBackwardNoInterruptA,
|
|
951
1443
|
handleJumpIfNotExcMatchA,
|
|
952
1444
|
handleNotTaken,
|
|
953
|
-
handleInstrumentedNotTakenA
|
|
1445
|
+
handleInstrumentedNotTakenA,
|
|
1446
|
+
resolveBoolChainFrames
|
|
954
1447
|
};
|