depyo 1.0.3 → 1.2.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/README.md +160 -83
- package/lib/PycDecompiler.js +761 -1
- package/lib/PycReader.js +10 -1
- package/lib/PythonObject.js +10 -2
- package/lib/ast/ast_node.js +130 -21
- package/lib/bytecode/python_3_14.js +1 -1
- package/lib/handlers/binary_ops.js +9 -0
- package/lib/handlers/comparisons.js +3 -10
- package/lib/handlers/exceptions_blocks.js +7 -2
- package/lib/handlers/function_calls.js +24 -5
- package/lib/handlers/function_class_build.js +11 -1
- package/lib/handlers/load_store_names.js +46 -1
- package/lib/handlers/misc_other.js +76 -4
- package/lib/handlers/stack_ops.js +4 -2
- package/lib/handlers/unary_ops.js +7 -0
- package/package.json +2 -2
package/lib/PycReader.js
CHANGED
|
@@ -38,13 +38,14 @@ const TypeSmallTuple = ')';
|
|
|
38
38
|
const TypeShortAscii = 'z';
|
|
39
39
|
const TypeShortAsciiInterned = 'Z';
|
|
40
40
|
const TypeObjectReference = 'r';
|
|
41
|
+
const TypeSlice = ':'; // CPython 3.14+: marshal'd slice constant (start, stop, step)
|
|
41
42
|
const KnownTypes = new Set([
|
|
42
43
|
TypeNull, TypeNone, TypeFalse, TypeTrue, TypeStopiter, TypeEllipsis,
|
|
43
44
|
TypeInt, TypeInt64, TypeFloat, TypeBinaryFloat, TypeComplex, TypeBinaryComplex,
|
|
44
45
|
TypeLong, TypeString, TypeInterned, TypeStringRef, TypeTuple, TypeList,
|
|
45
46
|
TypeDict, TypeCode, TypeCode2, TypeUnicode, TypeUnknown, TypeSet, TypeFrozenset,
|
|
46
47
|
TypeAscii, TypeAsciiInterned, TypeSmallTuple, TypeShortAscii, TypeShortAsciiInterned,
|
|
47
|
-
TypeObjectReference
|
|
48
|
+
TypeObjectReference, TypeSlice
|
|
48
49
|
]);
|
|
49
50
|
|
|
50
51
|
const MagicToVersion = {
|
|
@@ -590,6 +591,14 @@ class PycReader
|
|
|
590
591
|
obj.ClassName = "Py_FrozenSet";
|
|
591
592
|
obj.Value = frozenSet;
|
|
592
593
|
break;
|
|
594
|
+
case TypeSlice: {
|
|
595
|
+
const sliceStart = this.ReadObject();
|
|
596
|
+
const sliceStop = this.ReadObject();
|
|
597
|
+
const sliceStep = this.ReadObject();
|
|
598
|
+
obj.ClassName = "Py_Slice";
|
|
599
|
+
obj.Value = { start: sliceStart, stop: sliceStop, step: sliceStep };
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
593
602
|
case TypeCode:
|
|
594
603
|
case TypeCode2:
|
|
595
604
|
let codeObj = this.ReadCodeObject();
|
package/lib/PythonObject.js
CHANGED
|
@@ -57,7 +57,9 @@ class PythonObject {
|
|
|
57
57
|
.replace(/\\/g, '\\\\')
|
|
58
58
|
.replace(/\n/g, '\\n')
|
|
59
59
|
.replace(/\r/g, '\\r')
|
|
60
|
-
.replace(/\t/g, '\\t')
|
|
60
|
+
.replace(/\t/g, '\\t')
|
|
61
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, c =>
|
|
62
|
+
'\\x' + c.charCodeAt(0).toString(16).padStart(2, '0'));
|
|
61
63
|
let quote = '"';
|
|
62
64
|
if (escaped.includes('"')) {
|
|
63
65
|
if (!escaped.includes("'")) {
|
|
@@ -192,7 +194,13 @@ class PythonObject {
|
|
|
192
194
|
|
|
193
195
|
case "Py_Complex":
|
|
194
196
|
return `${this.Value[0]}+${this.Value[1]}j`;
|
|
195
|
-
|
|
197
|
+
|
|
198
|
+
case "Py_Slice": {
|
|
199
|
+
const part = (v) => v instanceof PythonObject ? v.toReprString() : String(v);
|
|
200
|
+
const { start, stop, step } = this.Value || {};
|
|
201
|
+
return `slice(${part(start)}, ${part(stop)}, ${part(step)})`;
|
|
202
|
+
}
|
|
203
|
+
|
|
196
204
|
default:
|
|
197
205
|
return ["Py_String"].includes(this.ClassName) ? this.Value.toString("ascii") : null;
|
|
198
206
|
}
|
package/lib/ast/ast_node.js
CHANGED
|
@@ -212,9 +212,14 @@ class ASTNodeList extends ASTNode {
|
|
|
212
212
|
if (this.emptyBlock()) {
|
|
213
213
|
result.add("pass");
|
|
214
214
|
} else {
|
|
215
|
+
// Defensive: a handler may have appended an undefined slot (e.g.
|
|
216
|
+
// a stack-pop that returned undefined on a malformed pyc).
|
|
217
|
+
// Without this, the sibling-link loop crashes on `undefined.prevSibling`
|
|
218
|
+
// and aborts rendering of the whole code object.
|
|
219
|
+
const list = this.list.filter(n => n);
|
|
215
220
|
let prevNode = null;
|
|
216
221
|
|
|
217
|
-
for (let node of
|
|
222
|
+
for (let node of list) {
|
|
218
223
|
if (prevNode) {
|
|
219
224
|
prevNode.nextSibling = node;
|
|
220
225
|
}
|
|
@@ -223,7 +228,7 @@ class ASTNodeList extends ASTNode {
|
|
|
223
228
|
}
|
|
224
229
|
prevNode = null;
|
|
225
230
|
|
|
226
|
-
for (let node of
|
|
231
|
+
for (let node of list) {
|
|
227
232
|
if (node.skip) {
|
|
228
233
|
continue;
|
|
229
234
|
}
|
|
@@ -258,7 +263,19 @@ class ASTNodeList extends ASTNode {
|
|
|
258
263
|
}
|
|
259
264
|
}
|
|
260
265
|
|
|
261
|
-
|
|
266
|
+
// Only join with `; ` between two simple (non-compound) statements
|
|
267
|
+
// that also happen to be on the same source line. A compound block
|
|
268
|
+
// (if/while/for/try/function/class) must never be semicolon-joined
|
|
269
|
+
// to a following sibling — doing so swallows the sibling into the
|
|
270
|
+
// block's body and breaks structure.
|
|
271
|
+
const prevIsCompound = prevNode instanceof ASTBlock ||
|
|
272
|
+
prevNode instanceof ASTFunction ||
|
|
273
|
+
prevNode instanceof ASTClass;
|
|
274
|
+
const nodeIsCompound = node instanceof ASTBlock ||
|
|
275
|
+
node instanceof ASTFunction ||
|
|
276
|
+
node instanceof ASTClass;
|
|
277
|
+
if (prevNode && spacing == 0 && sourceFragment.length == 1 &&
|
|
278
|
+
!prevIsCompound && !nodeIsCompound) {
|
|
262
279
|
result.lastLineAppend((prevNode ? "; " : "") + sourceFragment.toString(), false);
|
|
263
280
|
} else {
|
|
264
281
|
const blankCount = rawSpacing ? Math.max(0, spacing - 1) : (spacing > 1 ? 1 : 0);
|
|
@@ -465,7 +482,11 @@ class ASTObject extends ASTNode {
|
|
|
465
482
|
.replace(/\\/g, '\\\\')
|
|
466
483
|
.replace(/\n/g, '\\n')
|
|
467
484
|
.replace(/\r/g, '\\r')
|
|
468
|
-
.replace(/\t/g, '\\t')
|
|
485
|
+
.replace(/\t/g, '\\t')
|
|
486
|
+
// Any remaining C0/DEL byte must be hex-escaped; leaving a raw
|
|
487
|
+
// NUL or BEL in the source makes the output unparseable.
|
|
488
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, c =>
|
|
489
|
+
'\\x' + c.charCodeAt(0).toString(16).padStart(2, '0'));
|
|
469
490
|
let quote = '"';
|
|
470
491
|
if (escaped.includes('"')) {
|
|
471
492
|
if (!escaped.includes("'")) {
|
|
@@ -1103,7 +1124,13 @@ class ASTStore extends ASTNode {
|
|
|
1103
1124
|
if (inLambda) {
|
|
1104
1125
|
result.lastLineAppend(methodBody);
|
|
1105
1126
|
} else {
|
|
1106
|
-
|
|
1127
|
+
const bodyHasContent = methodBody && methodBody.toString().trim().length;
|
|
1128
|
+
const hasGlobals = codeObject.Globals && codeObject.Globals.size > 0;
|
|
1129
|
+
if (bodyHasContent) {
|
|
1130
|
+
result.add(methodBody);
|
|
1131
|
+
} else if (!hasGlobals) {
|
|
1132
|
+
result.add("pass");
|
|
1133
|
+
}
|
|
1107
1134
|
}
|
|
1108
1135
|
result.decreaseIndent();
|
|
1109
1136
|
|
|
@@ -2539,19 +2566,33 @@ class ASTBlock extends ASTNode {
|
|
|
2539
2566
|
result.add("pass");
|
|
2540
2567
|
}
|
|
2541
2568
|
} else {
|
|
2569
|
+
// Emit body nodes first, then trailing dedented handlers. If
|
|
2570
|
+
// every body node was `skip`-ed (e.g. Try body whose sole
|
|
2571
|
+
// child was consumed by ternary collapse), the block would
|
|
2572
|
+
// print `try:` with no body — inject a `pass` placeholder so
|
|
2573
|
+
// the resulting source stays parseable.
|
|
2574
|
+
let emittedBodyNode = false;
|
|
2575
|
+
const handlerTail = [];
|
|
2542
2576
|
renderNodes.map(node => {
|
|
2543
2577
|
if (!node || node.skip) {
|
|
2544
2578
|
return;
|
|
2545
2579
|
}
|
|
2546
2580
|
const isHandler = node.blockType == ASTBlock.BlockType.Except || node.blockType == ASTBlock.BlockType.Finally;
|
|
2547
2581
|
if (isHandler) {
|
|
2548
|
-
|
|
2549
|
-
result.add(node.codeFragment());
|
|
2550
|
-
result.increaseIndent();
|
|
2582
|
+
handlerTail.push(node);
|
|
2551
2583
|
} else {
|
|
2552
2584
|
result.add(node.codeFragment());
|
|
2585
|
+
emittedBodyNode = true;
|
|
2553
2586
|
}
|
|
2554
2587
|
});
|
|
2588
|
+
if (!emittedBodyNode) {
|
|
2589
|
+
result.add("pass");
|
|
2590
|
+
}
|
|
2591
|
+
for (const h of handlerTail) {
|
|
2592
|
+
result.decreaseIndent();
|
|
2593
|
+
result.add(h.codeFragment());
|
|
2594
|
+
result.increaseIndent();
|
|
2595
|
+
}
|
|
2555
2596
|
}
|
|
2556
2597
|
} else {
|
|
2557
2598
|
result.add("pass");
|
|
@@ -2566,6 +2607,17 @@ class ASTBlock extends ASTNode {
|
|
|
2566
2607
|
}
|
|
2567
2608
|
}
|
|
2568
2609
|
|
|
2610
|
+
// Predicate: can this node legally appear as one branch of a ternary `x if c else y`?
|
|
2611
|
+
// Block-shaped nodes render multi-line and an ASTKeyword (pass/break/continue) is a
|
|
2612
|
+
// statement, not an expression — appending " if c else y" to either is invalid Python.
|
|
2613
|
+
function isTernaryExprNode(node) {
|
|
2614
|
+
if (node == null) return false;
|
|
2615
|
+
if (node instanceof ASTBlock) return false;
|
|
2616
|
+
if (node instanceof ASTKeyword) return false;
|
|
2617
|
+
if (node instanceof ASTReturn) return false;
|
|
2618
|
+
return true;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2569
2621
|
class ASTCondBlock extends ASTBlock {
|
|
2570
2622
|
static InitCondition = {
|
|
2571
2623
|
Uninited: 0,
|
|
@@ -2600,7 +2652,23 @@ class ASTCondBlock extends ASTBlock {
|
|
|
2600
2652
|
|
|
2601
2653
|
codeFragment() {
|
|
2602
2654
|
let result = new PycResult("", true);
|
|
2655
|
+
// Cycle guard: if a control-flow handler accidentally nests a block
|
|
2656
|
+
// inside its own descendants, the recursive render would blow the JS
|
|
2657
|
+
// stack and abort the enclosing code object. Detect re-entry on the
|
|
2658
|
+
// same instance, emit a sentinel, and let the rest of the file render.
|
|
2659
|
+
if (this.__rendering) {
|
|
2660
|
+
result.add("###CYCLE###");
|
|
2661
|
+
return result;
|
|
2662
|
+
}
|
|
2663
|
+
this.__rendering = true;
|
|
2664
|
+
try {
|
|
2665
|
+
return this._codeFragmentImpl(result);
|
|
2666
|
+
} finally {
|
|
2667
|
+
this.__rendering = false;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2603
2670
|
|
|
2671
|
+
_codeFragmentImpl(result) {
|
|
2604
2672
|
// This is the assert case
|
|
2605
2673
|
if (this.blockType == ASTBlock.BlockType.If && this.negative && this.condition && this.nodes.length == 1 && this.nodes[0] instanceof ASTRaise) {
|
|
2606
2674
|
result.lastLineAppend("assert ", false);
|
|
@@ -2615,9 +2683,11 @@ class ASTCondBlock extends ASTBlock {
|
|
|
2615
2683
|
this.prevSibling instanceof ASTStore &&
|
|
2616
2684
|
this.blockType == ASTBlock.BlockType.If &&
|
|
2617
2685
|
this.nodes.length == 1 &&
|
|
2686
|
+
isTernaryExprNode(this.nodes[0]) &&
|
|
2618
2687
|
this.nextSibling instanceof ASTCondBlock &&
|
|
2619
2688
|
this.nextSibling?.type == ASTBlock.BlockType.Else &&
|
|
2620
2689
|
this.nextSibling?.nodes.length == 1 &&
|
|
2690
|
+
isTernaryExprNode(this.nextSibling.nodes[0]) &&
|
|
2621
2691
|
this.condition.line >= 0 &&
|
|
2622
2692
|
this.condition.line == this.nodes[0].line &&
|
|
2623
2693
|
this.condition.line == this.nextSibling?.nodes[0].line
|
|
@@ -2634,7 +2704,10 @@ class ASTCondBlock extends ASTBlock {
|
|
|
2634
2704
|
this.prevSibling == null &&
|
|
2635
2705
|
this.blockType == ASTBlock.BlockType.If &&
|
|
2636
2706
|
this.nodes.length == 1 &&
|
|
2707
|
+
isTernaryExprNode(this.nodes[0]) &&
|
|
2637
2708
|
this.nextSibling instanceof ASTReturn &&
|
|
2709
|
+
this.nextSibling.value &&
|
|
2710
|
+
isTernaryExprNode(this.nextSibling.value) &&
|
|
2638
2711
|
this.condition.line == this.nodes[0].line &&
|
|
2639
2712
|
this.condition.line == this.nextSibling?.value.line
|
|
2640
2713
|
) {
|
|
@@ -3137,12 +3210,27 @@ class ASTFormattedValue extends ASTNode {
|
|
|
3137
3210
|
return this.m_format_spec;
|
|
3138
3211
|
}
|
|
3139
3212
|
|
|
3140
|
-
codeFragment() {
|
|
3213
|
+
codeFragment(outerQuote = null) {
|
|
3141
3214
|
// Format: {value} or {value!r} or {value:.2f}
|
|
3215
|
+
// `outerQuote` is set only when this FormattedValue is being rendered
|
|
3216
|
+
// inside an enclosing f-string; nested strings must then use the
|
|
3217
|
+
// opposite delimiter so the outer string does not close prematurely.
|
|
3218
|
+
const innerQuote = outerQuote === '"' ? "'" :
|
|
3219
|
+
outerQuote === "'" ? '"' : '"';
|
|
3142
3220
|
let result = "{";
|
|
3143
3221
|
|
|
3144
3222
|
if (this.m_val) {
|
|
3145
|
-
|
|
3223
|
+
if (this.m_val instanceof ASTJoinedStr) {
|
|
3224
|
+
// Only force a flipped quote when we actually have an outer
|
|
3225
|
+
// f-string; otherwise let the nested string keep its default.
|
|
3226
|
+
if (outerQuote) {
|
|
3227
|
+
result += this.m_val.codeFragment(innerQuote);
|
|
3228
|
+
} else {
|
|
3229
|
+
result += this.m_val.codeFragment();
|
|
3230
|
+
}
|
|
3231
|
+
} else {
|
|
3232
|
+
result += this.m_val.codeFragment();
|
|
3233
|
+
}
|
|
3146
3234
|
}
|
|
3147
3235
|
|
|
3148
3236
|
// Add conversion flag (!s, !r, !a)
|
|
@@ -3163,7 +3251,10 @@ class ASTFormattedValue extends ASTNode {
|
|
|
3163
3251
|
result += ":";
|
|
3164
3252
|
// Format spec can be ASTJoinedStr (nested f-string) or string constant
|
|
3165
3253
|
if (this.m_format_spec instanceof ASTJoinedStr) {
|
|
3166
|
-
|
|
3254
|
+
// Format-spec content is already inside the outer f-string's
|
|
3255
|
+
// braces, so its literal parts must not use the outer quote.
|
|
3256
|
+
const specQuote = outerQuote ? innerQuote : '"';
|
|
3257
|
+
result += this.m_format_spec.codeFragment(specQuote, true);
|
|
3167
3258
|
} else if (this.m_format_spec instanceof ASTObject) {
|
|
3168
3259
|
// String constant like ".2f"
|
|
3169
3260
|
result += this.m_format_spec.object.Value;
|
|
@@ -3196,9 +3287,14 @@ class ASTJoinedStr extends ASTNode {
|
|
|
3196
3287
|
get lastLine() {
|
|
3197
3288
|
return this.values[this.values.length - 1]?.lastLine;
|
|
3198
3289
|
}
|
|
3199
|
-
codeFragment() {
|
|
3290
|
+
codeFragment(quoteChar = '"', bareInnerForFormatSpec = false) {
|
|
3200
3291
|
// PEP 750 t-strings use t"..." prefix; otherwise f-string.
|
|
3201
|
-
|
|
3292
|
+
// `quoteChar` lets nested f-strings pick the opposite delimiter so
|
|
3293
|
+
// they don't collide with the enclosing string. `bareInnerForFormatSpec`
|
|
3294
|
+
// skips the f"..." wrapper entirely (used when this ASTJoinedStr is a
|
|
3295
|
+
// format spec nested inside {...}).
|
|
3296
|
+
const prefix = this.isTemplateString ? 't' : 'f';
|
|
3297
|
+
let result = bareInnerForFormatSpec ? "" : `${prefix}${quoteChar}`;
|
|
3202
3298
|
|
|
3203
3299
|
// Values are in reverse order (BUILD_STRING pops from stack)
|
|
3204
3300
|
// So we need to reverse them
|
|
@@ -3232,8 +3328,10 @@ class ASTJoinedStr extends ASTNode {
|
|
|
3232
3328
|
let prefix = prevStr.substring(0, prevStr.length - match[0].length);
|
|
3233
3329
|
|
|
3234
3330
|
// Remove the previously added literal and replace with prefix + {var=}
|
|
3331
|
+
let quoteEsc = quoteChar === '"' ? /"/g : /'/g;
|
|
3332
|
+
let quoteReplace = quoteChar === '"' ? '\\"' : "\\'";
|
|
3235
3333
|
let beforeLiteral = result.lastIndexOf(prevStr.replace(/\\/g, '\\\\')
|
|
3236
|
-
.replace(
|
|
3334
|
+
.replace(quoteEsc, quoteReplace).replace(/\n/g, '\\n').replace(/\t/g, '\\t'));
|
|
3237
3335
|
|
|
3238
3336
|
if (beforeLiteral !== -1) {
|
|
3239
3337
|
result = result.substring(0, beforeLiteral);
|
|
@@ -3242,7 +3340,7 @@ class ASTJoinedStr extends ASTNode {
|
|
|
3242
3340
|
// Add prefix (escaped) if present
|
|
3243
3341
|
if (prefix) {
|
|
3244
3342
|
let escapedPrefix = prefix.replace(/\\/g, '\\\\');
|
|
3245
|
-
escapedPrefix = escapedPrefix.replace(
|
|
3343
|
+
escapedPrefix = escapedPrefix.replace(quoteEsc, quoteReplace);
|
|
3246
3344
|
escapedPrefix = escapedPrefix.replace(/\n/g, '\\n');
|
|
3247
3345
|
escapedPrefix = escapedPrefix.replace(/\t/g, '\\t');
|
|
3248
3346
|
result += escapedPrefix;
|
|
@@ -3254,16 +3352,25 @@ class ASTJoinedStr extends ASTNode {
|
|
|
3254
3352
|
}
|
|
3255
3353
|
}
|
|
3256
3354
|
|
|
3257
|
-
// {expression} part
|
|
3258
|
-
|
|
3259
|
-
|
|
3355
|
+
// {expression} part - pass our own quote char so nested
|
|
3356
|
+
// f-strings / strings inside pick a compatible delimiter.
|
|
3357
|
+
result += value.codeFragment(quoteChar);
|
|
3358
|
+
// result above calls ASTFormattedValue.codeFragment(outerQuote).
|
|
3359
|
+
} else if (value instanceof ASTObject && ['Py_String', 'Py_Unicode'].includes(value.object?.ClassName)) {
|
|
3260
3360
|
// Literal string part - need to escape special chars
|
|
3261
3361
|
let str = value.object.Value;
|
|
3262
|
-
// Escape backslashes and
|
|
3362
|
+
// Escape backslashes and the current quote char
|
|
3263
3363
|
str = str.replace(/\\/g, '\\\\');
|
|
3264
|
-
|
|
3364
|
+
if (quoteChar === '"') {
|
|
3365
|
+
str = str.replace(/"/g, '\\"');
|
|
3366
|
+
} else {
|
|
3367
|
+
str = str.replace(/'/g, "\\'");
|
|
3368
|
+
}
|
|
3265
3369
|
str = str.replace(/\n/g, '\\n');
|
|
3370
|
+
str = str.replace(/\r/g, '\\r');
|
|
3266
3371
|
str = str.replace(/\t/g, '\\t');
|
|
3372
|
+
str = str.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, c =>
|
|
3373
|
+
'\\x' + c.charCodeAt(0).toString(16).padStart(2, '0'));
|
|
3267
3374
|
result += str;
|
|
3268
3375
|
} else {
|
|
3269
3376
|
// Fallback for unexpected types
|
|
@@ -3271,7 +3378,9 @@ class ASTJoinedStr extends ASTNode {
|
|
|
3271
3378
|
}
|
|
3272
3379
|
}
|
|
3273
3380
|
|
|
3274
|
-
|
|
3381
|
+
if (!bareInnerForFormatSpec) {
|
|
3382
|
+
result += quoteChar;
|
|
3383
|
+
}
|
|
3275
3384
|
return result;
|
|
3276
3385
|
}
|
|
3277
3386
|
|
|
@@ -7,7 +7,7 @@ const opcodes = [
|
|
|
7
7
|
[0, new OpCode(OpCodes.CACHE, "CACHE")],
|
|
8
8
|
[1, new OpCode(OpCodes.BINARY_SLICE, "BINARY_SLICE")],
|
|
9
9
|
[2, new OpCode(OpCodes.BUILD_TEMPLATE, "BUILD_TEMPLATE")],
|
|
10
|
-
[4, new OpCode(OpCodes.
|
|
10
|
+
[4, new OpCode(OpCodes.CALL_FUNCTION_EX_A, "CALL_FUNCTION_EX", {HasArgument: true})],
|
|
11
11
|
[5, new OpCode(OpCodes.CHECK_EG_MATCH, "CHECK_EG_MATCH")],
|
|
12
12
|
[6, new OpCode(OpCodes.CHECK_EXC_MATCH, "CHECK_EXC_MATCH")],
|
|
13
13
|
[7, new OpCode(OpCodes.CLEANUP_THROW, "CLEANUP_THROW")],
|
|
@@ -2,6 +2,15 @@ const AST = require('../ast/ast_node');
|
|
|
2
2
|
|
|
3
3
|
function handleBinaryOpA()
|
|
4
4
|
{
|
|
5
|
+
// Python 3.14 fused BINARY_SUBSCR into BINARY_OP arg=26 (NB_SUBSCR).
|
|
6
|
+
if (this.code.Current.Argument === 26) {
|
|
7
|
+
let subscr = this.dataStack.pop();
|
|
8
|
+
let src = this.dataStack.pop();
|
|
9
|
+
let node = new AST.ASTSubscr(src, subscr);
|
|
10
|
+
node.line = this.code.Current.LineNo;
|
|
11
|
+
this.dataStack.push(node);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
5
14
|
let rVal = this.dataStack.pop();
|
|
6
15
|
let lVal = this.dataStack.pop();
|
|
7
16
|
let op = AST.ASTBinary.from_binary_op(this.code.Current.Argument);
|
|
@@ -33,17 +33,10 @@ function handleCompareOpA() {
|
|
|
33
33
|
let left = this.dataStack.pop();
|
|
34
34
|
let arg = this.code.Current.Argument;
|
|
35
35
|
if (this.object.Reader.versionCompare(3, 13) >= 0) {
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
const mapped = {
|
|
39
|
-
18: AST.ASTCompare.CompareOp.Less, // guards: n < 0
|
|
40
|
-
72: AST.ASTCompare.CompareOp.Equal, // len == N
|
|
41
|
-
88: AST.ASTCompare.CompareOp.Equal, // literal == value
|
|
42
|
-
148: AST.ASTCompare.CompareOp.Greater // guards: n > 0
|
|
43
|
-
};
|
|
44
|
-
arg = mapped[arg] ?? (arg & 0x0F);
|
|
36
|
+
// Py3.13: oparg = (op << 5) | (cast_bool<<4) | cache_flags. op ∈ 0..5.
|
|
37
|
+
arg >>= 5;
|
|
45
38
|
} else if (this.object.Reader.versionCompare(3, 12) >= 0) {
|
|
46
|
-
//
|
|
39
|
+
// Py3.12: oparg = (op << 4) | cache_flags. op ∈ 0..5.
|
|
47
40
|
arg >>= 4;
|
|
48
41
|
}
|
|
49
42
|
|
|
@@ -477,7 +477,8 @@ function handlePopExcept() {
|
|
|
477
477
|
this.curBlock = this.defBlock || exceptBlock;
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
|
|
480
|
+
// exceptBlock.nodes returns the block's m_nodes array directly (getter with no setter).
|
|
481
|
+
let bodyNodes = exceptBlock.nodes;
|
|
481
482
|
|
|
482
483
|
if (this.curBlock.blockType == AST.ASTBlock.BlockType.Try) {
|
|
483
484
|
const pendingEgType = this.egMatchTypeStack?.shift?.() || this.pendingEgMatchType;
|
|
@@ -502,7 +503,11 @@ function handlePopExcept() {
|
|
|
502
503
|
}
|
|
503
504
|
let catchBlock = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, exceptBlock.start, exceptBlock.end, matchType, false);
|
|
504
505
|
catchBlock.line = exceptBlock.line;
|
|
505
|
-
|
|
506
|
+
// Copy the handler body from the popped Except. `nodes` is getter-only,
|
|
507
|
+
// so mutate m_nodes directly — plain assignment here silently no-ops.
|
|
508
|
+
if (Array.isArray(bodyNodes) && bodyNodes.length) {
|
|
509
|
+
catchBlock.m_nodes.push(...bodyNodes);
|
|
510
|
+
}
|
|
506
511
|
catchBlock.isExceptStar = isExceptStar;
|
|
507
512
|
if (catchBlock.condition && typeof catchBlock.condition === 'object' && 'src' in catchBlock.condition) {
|
|
508
513
|
const storeSrc = catchBlock.condition.src;
|
|
@@ -297,7 +297,10 @@ function handleInstrumentedCallA() {
|
|
|
297
297
|
const topType = sourceTop?.constructor?.name || 'null';
|
|
298
298
|
console.log(`[CALL] func=${codeObj?.Name || codeObj?.QualName?.Value || '<?>'} sourceTop=${topType} compNode=${compNode?.constructor?.name || 'null'} hasSource=${!!codeObj?.SourceCode} pparams=${pparamList.length}`);
|
|
299
299
|
}
|
|
300
|
-
|
|
300
|
+
// Name is a PythonObject (Py_String/Py_Unicode), not a raw string — compare
|
|
301
|
+
// via toString() so the Set lookup actually matches.
|
|
302
|
+
const codeName = func.code?.object?.Name?.toString?.() || '';
|
|
303
|
+
const looksLikeComp = compNames.has(codeName) ||
|
|
301
304
|
sourceTop instanceof AST.ASTComprehension ||
|
|
302
305
|
compNode instanceof AST.ASTComprehension ||
|
|
303
306
|
(sourceTop instanceof AST.ASTReturn && sourceTop.value instanceof AST.ASTComprehension);
|
|
@@ -313,6 +316,15 @@ function handleInstrumentedCallA() {
|
|
|
313
316
|
if (!(resultNode instanceof AST.ASTComprehension) && codeObj?.SourceCode) {
|
|
314
317
|
let yieldExpr = null;
|
|
315
318
|
let loopVar = null;
|
|
319
|
+
// Seed async-ness from the code flags: async comprehensions and
|
|
320
|
+
// async generator expressions carry CO_ASYNC_GENERATOR /
|
|
321
|
+
// CO_COROUTINE on the inner code object, so use that as the
|
|
322
|
+
// default in case the inner tree lacks an explicit AsyncFor
|
|
323
|
+
// block (e.g. pre-3.8 async comprehensions use GET_ANEXT +
|
|
324
|
+
// STORE instead of a FOR_ITER-driven iter block).
|
|
325
|
+
const flags = codeObj?.Flags | 0;
|
|
326
|
+
let isAsyncLoop = !!(flags & AST.ASTFunction.CodeFlags.CO_ASYNC_GENERATOR) ||
|
|
327
|
+
!!(flags & AST.ASTFunction.CodeFlags.CO_COROUTINE);
|
|
316
328
|
const searchNodes = (nodes) => {
|
|
317
329
|
if (!nodes) return;
|
|
318
330
|
for (const node of nodes) {
|
|
@@ -322,8 +334,13 @@ function handleInstrumentedCallA() {
|
|
|
322
334
|
if (node instanceof AST.ASTStore && !loopVar) {
|
|
323
335
|
loopVar = node.dest;
|
|
324
336
|
}
|
|
325
|
-
if (node instanceof AST.ASTIterBlock &&
|
|
326
|
-
|
|
337
|
+
if (node instanceof AST.ASTIterBlock &&
|
|
338
|
+
(node.blockType === AST.ASTBlock.BlockType.AsyncFor ||
|
|
339
|
+
node.blockType === AST.ASTBlock.BlockType.For)) {
|
|
340
|
+
if (node.index) {
|
|
341
|
+
loopVar = node.index;
|
|
342
|
+
isAsyncLoop = (node.blockType === AST.ASTBlock.BlockType.AsyncFor);
|
|
343
|
+
}
|
|
327
344
|
if (node.nodes) searchNodes(node.nodes);
|
|
328
345
|
}
|
|
329
346
|
if (node?.nodes) searchNodes(node.nodes);
|
|
@@ -356,13 +373,14 @@ function handleInstrumentedCallA() {
|
|
|
356
373
|
const varName = op.Name?.toString?.() || '';
|
|
357
374
|
if (varName) {
|
|
358
375
|
loopVar = new AST.ASTName(varName);
|
|
376
|
+
isAsyncLoop = true;
|
|
359
377
|
break;
|
|
360
378
|
}
|
|
361
379
|
}
|
|
362
380
|
}
|
|
363
381
|
}
|
|
364
382
|
if (global.g_cliArgs?.debug) {
|
|
365
|
-
console.log(`[CALL-RECONSTRUCT] ${codeObj.Name?.toString?.()} yield=${yieldExpr?.constructor?.name} loopVar=${loopVar?.name}`);
|
|
383
|
+
console.log(`[CALL-RECONSTRUCT] ${codeObj.Name?.toString?.()} yield=${yieldExpr?.constructor?.name} loopVar=${loopVar?.name} isAsyncLoop=${isAsyncLoop}`);
|
|
366
384
|
}
|
|
367
385
|
if (yieldExpr && loopVar) {
|
|
368
386
|
let kind = AST.ASTComprehension.GENERATOR;
|
|
@@ -377,7 +395,8 @@ function handleInstrumentedCallA() {
|
|
|
377
395
|
comp = new AST.ASTComprehension(yieldExpr);
|
|
378
396
|
}
|
|
379
397
|
comp.kind = kind;
|
|
380
|
-
|
|
398
|
+
const genBlockType = isAsyncLoop ? AST.ASTBlock.BlockType.AsyncFor : AST.ASTBlock.BlockType.For;
|
|
399
|
+
let gen = new AST.ASTIterBlock(genBlockType, 0, 0, new AST.ASTName('.0'));
|
|
381
400
|
gen.index = loopVar;
|
|
382
401
|
gen.comprehension = true;
|
|
383
402
|
comp.addGenerator(gen);
|
|
@@ -379,7 +379,17 @@ function handleBuildConstKeyMapA() {
|
|
|
379
379
|
function handleBuildStringA() {
|
|
380
380
|
let values = [];
|
|
381
381
|
for (let idx = 0; idx < this.code.Current.Argument; idx++) {
|
|
382
|
-
|
|
382
|
+
let v = this.dataStack.pop();
|
|
383
|
+
// Unwrap premature single-FormattedValue JoinedStr wrappers. These arise
|
|
384
|
+
// when FORMAT_SIMPLE's lookahead (hasUpcomingStringBuild) bailed early
|
|
385
|
+
// on a CALL_A that actually belongs to a later interpolation in this
|
|
386
|
+
// same BUILD_STRING — rendering the wrapper would otherwise produce
|
|
387
|
+
// `f"f"{expr}""` nested inside the outer f-string.
|
|
388
|
+
if (v instanceof AST.ASTJoinedStr && v.values && v.values.length === 1 &&
|
|
389
|
+
v.values[0] instanceof AST.ASTFormattedValue) {
|
|
390
|
+
v = v.values[0];
|
|
391
|
+
}
|
|
392
|
+
values.push(v);
|
|
383
393
|
}
|
|
384
394
|
|
|
385
395
|
let stringNode = new AST.ASTJoinedStr(values);
|
|
@@ -182,6 +182,22 @@ function handleLoadGlobalA() {
|
|
|
182
182
|
this.dataStack.push(node);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
function handleLoadFromDictOrGlobalsA() {
|
|
186
|
+
// Python 3.12+: pops the class-body locals dict pushed by LOAD_LOCALS,
|
|
187
|
+
// looks up co_names[A] there, falling back to globals. For source
|
|
188
|
+
// reconstruction we render it as a bare name reference.
|
|
189
|
+
if (this.dataStack.top() instanceof AST.ASTLocals) {
|
|
190
|
+
this.dataStack.pop();
|
|
191
|
+
}
|
|
192
|
+
const varName = this.code.Current.Name || "";
|
|
193
|
+
if (!varName.length) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let node = new AST.ASTName(varName);
|
|
197
|
+
node.line = this.code.Current.LineNo;
|
|
198
|
+
this.dataStack.push(node);
|
|
199
|
+
}
|
|
200
|
+
|
|
185
201
|
function handleLoadFromDictOrDerefA() {
|
|
186
202
|
// Python 3.12+: LOAD_FROM_DICT_OR_DEREF consumes the locals dict from TOS
|
|
187
203
|
// (pushed by the preceding LOAD_LOCALS) and resolves the name against it,
|
|
@@ -253,9 +269,15 @@ function handleLoadSpecialA() {
|
|
|
253
269
|
}
|
|
254
270
|
|
|
255
271
|
// For __exit__ (oparg 1): save the context manager expression for with-block
|
|
256
|
-
// This is called BEFORE __enter__ in 3.14 bytecode pattern
|
|
272
|
+
// This is called BEFORE __enter__ in 3.14 bytecode pattern.
|
|
273
|
+
// The 3.14 with-prologue uses LOAD_FAST(ctx); COPY 1; LOAD_SPECIAL 1, which
|
|
274
|
+
// looks like the LOAD+COPY 1 match-subject idiom — clear the candidate so
|
|
275
|
+
// a later COMPARE_OP inside the with-body doesn't get wrapped in a
|
|
276
|
+
// synthetic `match ctx:` rooted on the context manager.
|
|
257
277
|
if (oparg === 1) {
|
|
258
278
|
this._py314WithContextMgr = obj;
|
|
279
|
+
this.potentialMatchSubject = null;
|
|
280
|
+
this.matchCandidateStart = -1;
|
|
259
281
|
}
|
|
260
282
|
|
|
261
283
|
// For __enter__ (oparg 0): create with-block using saved context manager
|
|
@@ -478,6 +500,28 @@ function processStore(nameOverride) {
|
|
|
478
500
|
}
|
|
479
501
|
}
|
|
480
502
|
|
|
503
|
+
// `case _ as name:` final clause: no preceding pattern ops, no LOAD
|
|
504
|
+
// before the STORE, and we're inside a match. CPython emits a bare STORE
|
|
505
|
+
// to bind the subject still living on the stack.
|
|
506
|
+
if (this.currentMatch && !this.currentCase && !this.inMatchPattern &&
|
|
507
|
+
(this.patternOps?.length || 0) === 0) {
|
|
508
|
+
const prevId = this.code.Prev?.OpCodeID;
|
|
509
|
+
const isAfterReturn = prevId == this.OpCodes.RETURN_VALUE ||
|
|
510
|
+
prevId == this.OpCodes.RETURN_VALUE_A ||
|
|
511
|
+
prevId == this.OpCodes.RETURN_CONST_A;
|
|
512
|
+
if (isAfterReturn) {
|
|
513
|
+
const wildcard = new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_');
|
|
514
|
+
const asPattern = new AST.ASTPattern(AST.ASTPattern.PatternType.As, {
|
|
515
|
+
pattern: wildcard,
|
|
516
|
+
name: currentName
|
|
517
|
+
});
|
|
518
|
+
this.currentCase = new AST.ASTCase(asPattern, null, null);
|
|
519
|
+
this.currentCase.line = this.code.Current.LineNo;
|
|
520
|
+
this.caseBodyStartIndex = this.curBlock?.nodes?.length || 0;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
481
525
|
if (global.g_cliArgs?.debug && currentName && (currentName === 'b' || currentName === 'i')) {
|
|
482
526
|
console.log(`[processStore] varName=${currentName}, curBlock=${this.curBlock.type_str}, inited=${this.curBlock.inited}, unpack=${this.unpack}`);
|
|
483
527
|
console.log(` Block stack: ${this.blocks.map((b,i) => `[${i}]${b.type_str}(inited=${b.inited})`).join(' → ')}`);
|
|
@@ -832,6 +876,7 @@ module.exports = {
|
|
|
832
876
|
handleLoadZeroSuperAttrA,
|
|
833
877
|
handleLoadZeroSuperMethodA,
|
|
834
878
|
handleLoadFromDictOrDerefA,
|
|
879
|
+
handleLoadFromDictOrGlobalsA,
|
|
835
880
|
handleStoreAttrA,
|
|
836
881
|
handleStoreDerefA,
|
|
837
882
|
handleStoreFastA,
|