depyo 1.0.3 → 1.1.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/lib/PycDecompiler.js +648 -1
- package/lib/PythonObject.js +3 -1
- package/lib/ast/ast_node.js +91 -19
- 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/misc_other.js +37 -1
- package/package.json +2 -2
package/lib/PycDecompiler.js
CHANGED
|
@@ -150,6 +150,8 @@ class PycDecompiler {
|
|
|
150
150
|
this.enrichGenericAnnotations(functonBody);
|
|
151
151
|
this.rewriteClassDefinitions(functonBody);
|
|
152
152
|
this.removeNullSentinelComparisons(functonBody);
|
|
153
|
+
this.cleanupExcMatchArtifacts(functonBody);
|
|
154
|
+
this.hoistNestedExceptBlocks(functonBody);
|
|
153
155
|
this.dedupeExceptHandlers(functonBody);
|
|
154
156
|
this.removeDuplicateReturns(functonBody);
|
|
155
157
|
|
|
@@ -428,7 +430,19 @@ class PycDecompiler {
|
|
|
428
430
|
if (offset === undefined) {
|
|
429
431
|
return;
|
|
430
432
|
}
|
|
431
|
-
|
|
433
|
+
// Exclude cleanup-only handler entries (depth>0 whose start is a RERAISE site,
|
|
434
|
+
// not a PUSH_EXC_INFO) from the try-block span. Those entries protect CPython's
|
|
435
|
+
// implicit re-raise tail, not a user-written except, and must not be treated
|
|
436
|
+
// as handler bodies when sizing the enclosing try.
|
|
437
|
+
const isUserHandlerEntry = (e) => {
|
|
438
|
+
if (!e || e.depth <= 0) return false;
|
|
439
|
+
const startInstr = this.code.PeekInstructionAtOffset(e.start);
|
|
440
|
+
return startInstr && startInstr.OpCodeID === this.OpCodes.PUSH_EXC_INFO;
|
|
441
|
+
};
|
|
442
|
+
const maxHandlerEnd = this.maxExceptionHandlerEnd || Math.max(
|
|
443
|
+
...entries.filter(e => e.depth > 0 && isUserHandlerEntry(e)).map(e => e.end || 0),
|
|
444
|
+
0
|
|
445
|
+
);
|
|
432
446
|
|
|
433
447
|
// Build set of WITH_EXCEPT_START handler ranges (lazily, once per code object)
|
|
434
448
|
if (!this._withExceptRanges) {
|
|
@@ -579,6 +593,16 @@ class PycDecompiler {
|
|
|
579
593
|
}
|
|
580
594
|
}
|
|
581
595
|
}
|
|
596
|
+
// Skip depth>0 entries whose start is a RERAISE cleanup site rather than
|
|
597
|
+
// a user-level PUSH_EXC_INFO handler. These appear in CPython 3.11+ as
|
|
598
|
+
// protected tails around `RERAISE 0` or `COPY n; POP_EXCEPT; RERAISE 1`
|
|
599
|
+
// and have no source-level except counterpart.
|
|
600
|
+
if (entry.depth > 0 && !isUserHandlerEntry(entry)) {
|
|
601
|
+
if (g_cliArgs?.debug) {
|
|
602
|
+
console.log(`[EnsureExcBlocks] Skipping cleanup-only handler entry at ${entry.start} (not PUSH_EXC_INFO)`);
|
|
603
|
+
}
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
582
606
|
const handlerEnd = entry.depth === 0
|
|
583
607
|
? (maxHandlerEnd || entry.end || this.code.LastOffset || this.object.CodeSize || 0)
|
|
584
608
|
: (entry.end || maxHandlerEnd || this.code.LastOffset || this.object.CodeSize || 0);
|
|
@@ -644,6 +668,54 @@ class PycDecompiler {
|
|
|
644
668
|
if (!next) break;
|
|
645
669
|
end = next.end;
|
|
646
670
|
}
|
|
671
|
+
// The exception-table entry only covers the prologue (the protected
|
|
672
|
+
// region around CHECK_EXC_MATCH). The actual handler body — user
|
|
673
|
+
// statements, POP_EXCEPT, and the terminating exit — lives past the
|
|
674
|
+
// entry end. Walk forward to include POP_EXCEPT and the following
|
|
675
|
+
// terminator (RETURN/JUMP_FORWARD/RERAISE) so the Except block
|
|
676
|
+
// captures the body that renders as user code.
|
|
677
|
+
if (this.code && this.OpCodes) {
|
|
678
|
+
let cursor = end;
|
|
679
|
+
let steps = 0;
|
|
680
|
+
let sawHandlerExit = false;
|
|
681
|
+
while (cursor >= 0 && steps++ < 500) {
|
|
682
|
+
const instr = this.code.PeekInstructionAtOffset(cursor);
|
|
683
|
+
if (!instr) break;
|
|
684
|
+
const op = instr.OpCodeID;
|
|
685
|
+
// Stop if we enter another handler's prologue.
|
|
686
|
+
if (op === this.OpCodes.PUSH_EXC_INFO) break;
|
|
687
|
+
if (op === this.OpCodes.POP_EXCEPT) {
|
|
688
|
+
sawHandlerExit = true;
|
|
689
|
+
end = instr.Offset + (instr.Size || 2);
|
|
690
|
+
cursor = end;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (sawHandlerExit && (
|
|
694
|
+
op === this.OpCodes.RETURN_VALUE ||
|
|
695
|
+
op === this.OpCodes.RETURN_VALUE_A ||
|
|
696
|
+
op === this.OpCodes.RETURN_CONST_A ||
|
|
697
|
+
op === this.OpCodes.JUMP_FORWARD ||
|
|
698
|
+
op === this.OpCodes.JUMP_FORWARD_A ||
|
|
699
|
+
op === this.OpCodes.JUMP_BACKWARD ||
|
|
700
|
+
op === this.OpCodes.JUMP_BACKWARD_A ||
|
|
701
|
+
op === this.OpCodes.RERAISE ||
|
|
702
|
+
op === this.OpCodes.RERAISE_A)) {
|
|
703
|
+
end = instr.Offset + (instr.Size || 2);
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
if (sawHandlerExit) {
|
|
707
|
+
// Skip trailing CACHE/POP_TOP cleanup that sometimes sits
|
|
708
|
+
// between POP_EXCEPT and the terminator on 3.11+.
|
|
709
|
+
if (op === this.OpCodes.CACHE ||
|
|
710
|
+
op === this.OpCodes.POP_TOP) {
|
|
711
|
+
cursor = instr.Offset + (instr.Size || 2);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
cursor = instr.Offset + (instr.Size || 2);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
647
719
|
return end;
|
|
648
720
|
}
|
|
649
721
|
|
|
@@ -1653,11 +1725,45 @@ class PycDecompiler {
|
|
|
1653
1725
|
if (hasDataclass) {
|
|
1654
1726
|
node.addDecorator(new AST.ASTName('dataclass'));
|
|
1655
1727
|
}
|
|
1728
|
+
} else if (node.src instanceof AST.ASTCall && this.isDecoratedClassCall(node.src)) {
|
|
1729
|
+
// Pattern: `Name = decorator(ClassLiteral[, ...])` produced by Py3.12+
|
|
1730
|
+
// dataclass/decorator-of-class flows. Promote to a real class with
|
|
1731
|
+
// the call-of-decorator attached as an @decorator.
|
|
1732
|
+
const call = node.src;
|
|
1733
|
+
const classNode = call.pparams[0];
|
|
1734
|
+
const decoratorExpr = this._buildDecoratorExpr(call);
|
|
1735
|
+
node.src = classNode;
|
|
1736
|
+
this.cleanupClassBody(classNode);
|
|
1737
|
+
if (decoratorExpr) {
|
|
1738
|
+
node.addDecorator(decoratorExpr);
|
|
1739
|
+
}
|
|
1656
1740
|
} else if (node.src instanceof AST.ASTClass) {
|
|
1657
1741
|
this.cleanupClassBody(node.src);
|
|
1658
1742
|
}
|
|
1659
1743
|
}
|
|
1660
1744
|
|
|
1745
|
+
isDecoratedClassCall(call) {
|
|
1746
|
+
if (!(call instanceof AST.ASTCall)) return false;
|
|
1747
|
+
if (call.func instanceof AST.ASTClass) return false;
|
|
1748
|
+
if (!call.pparams || call.pparams.length < 1) return false;
|
|
1749
|
+
if (!(call.pparams[0] instanceof AST.ASTClass)) return false;
|
|
1750
|
+
// Any further pparams would become positional args to the decorator,
|
|
1751
|
+
// which cannot be expressed as `@decorator(class)` sugar. Reject.
|
|
1752
|
+
return call.pparams.length === 1;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
_buildDecoratorExpr(call) {
|
|
1756
|
+
// `decorator(class)` → `@decorator`
|
|
1757
|
+
// `decorator(class, kw=val, ...)` → `@decorator(kw=val, ...)`
|
|
1758
|
+
const kwargs = call.kwparams || [];
|
|
1759
|
+
if (kwargs.length === 0 && !call.hasVar && !call.hasKw) {
|
|
1760
|
+
return call.func;
|
|
1761
|
+
}
|
|
1762
|
+
const wrapper = new AST.ASTCall(call.func, [], kwargs);
|
|
1763
|
+
wrapper.line = call.line;
|
|
1764
|
+
return wrapper;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1661
1767
|
_childContainers(node) {
|
|
1662
1768
|
const containers = [];
|
|
1663
1769
|
if (node instanceof AST.ASTBlock) {
|
|
@@ -1801,6 +1907,539 @@ class PycDecompiler {
|
|
|
1801
1907
|
}
|
|
1802
1908
|
}
|
|
1803
1909
|
|
|
1910
|
+
// Rewrites 3.11+ CHECK_EXC_MATCH artifacts that leaked past block
|
|
1911
|
+
// reconstruction: unwraps `except <exc> EXC_MATCH <type>` conditions
|
|
1912
|
+
// down to `<type>`, rewrites sibling `while __exception__ EXC_MATCH T:`
|
|
1913
|
+
// blocks into `except T:` handlers on the preceding Try, and strips
|
|
1914
|
+
// trailing compiler-generated reraise/cleanup nodes (bare `raise`,
|
|
1915
|
+
// `e = None; del e`, empty `try: pass`). Runs once on the whole tree.
|
|
1916
|
+
cleanupExcMatchArtifacts(root) {
|
|
1917
|
+
// CHECK_EXC_MATCH only exists in Py3.11+; earlier versions never produce
|
|
1918
|
+
// the EXC_MATCH compare op or the __exception__ sentinel, so skip.
|
|
1919
|
+
if (this.object.Reader.versionCompare(3, 11) < 0) return;
|
|
1920
|
+
|
|
1921
|
+
const EXC_MATCH = AST.ASTCompare.CompareOp.Exception;
|
|
1922
|
+
|
|
1923
|
+
const unwrapExcMatch = (cond) => {
|
|
1924
|
+
if (!cond) return cond;
|
|
1925
|
+
// The condition may have been wrapped in ASTStore (alias binding) -
|
|
1926
|
+
// unwrap first, then strip the EXC_MATCH compare.
|
|
1927
|
+
let inner = cond;
|
|
1928
|
+
if (inner instanceof AST.ASTStore && inner.src) {
|
|
1929
|
+
inner = inner.src;
|
|
1930
|
+
}
|
|
1931
|
+
if (inner instanceof AST.ASTCompare && inner.op === EXC_MATCH) {
|
|
1932
|
+
return inner.right;
|
|
1933
|
+
}
|
|
1934
|
+
return cond;
|
|
1935
|
+
};
|
|
1936
|
+
|
|
1937
|
+
const isExcMatchCompare = (node) =>
|
|
1938
|
+
node instanceof AST.ASTCompare && node.op === EXC_MATCH;
|
|
1939
|
+
|
|
1940
|
+
const isExceptionName = (node) =>
|
|
1941
|
+
node instanceof AST.ASTName && node.name === '__exception__';
|
|
1942
|
+
|
|
1943
|
+
const containsExceptionRef = (node, depth = 0) => {
|
|
1944
|
+
if (!node || depth > 4) return false;
|
|
1945
|
+
if (isExceptionName(node)) return true;
|
|
1946
|
+
if (node instanceof AST.ASTCompare || node instanceof AST.ASTBinary) {
|
|
1947
|
+
return containsExceptionRef(node.left, depth + 1) ||
|
|
1948
|
+
containsExceptionRef(node.right, depth + 1);
|
|
1949
|
+
}
|
|
1950
|
+
if (node instanceof AST.ASTCall) {
|
|
1951
|
+
return containsExceptionRef(node.func, depth + 1) ||
|
|
1952
|
+
(node.pparams || []).some(p => containsExceptionRef(p, depth + 1));
|
|
1953
|
+
}
|
|
1954
|
+
if (node instanceof AST.ASTStore) {
|
|
1955
|
+
return containsExceptionRef(node.src, depth + 1);
|
|
1956
|
+
}
|
|
1957
|
+
return false;
|
|
1958
|
+
};
|
|
1959
|
+
|
|
1960
|
+
const isEmptyTryBlock = (node) =>
|
|
1961
|
+
node instanceof AST.ASTBlock &&
|
|
1962
|
+
node.blockType === AST.ASTBlock.BlockType.Try &&
|
|
1963
|
+
(node.nodes.length === 0 ||
|
|
1964
|
+
(node.nodes.length === 1 &&
|
|
1965
|
+
node.nodes[0] instanceof AST.ASTKeyword &&
|
|
1966
|
+
node.nodes[0].key === AST.ASTKeyword.Word.Pass));
|
|
1967
|
+
|
|
1968
|
+
const isOrphanExceptionStmt = (node) => {
|
|
1969
|
+
// Bare `raise` with no params → fall-through RERAISE artifact
|
|
1970
|
+
if (node instanceof AST.ASTRaise && (!node.params || node.params.length === 0)) {
|
|
1971
|
+
return true;
|
|
1972
|
+
}
|
|
1973
|
+
// `__exception__(None, None, None)` or `__exception__` alone
|
|
1974
|
+
if (node instanceof AST.ASTCall && isExceptionName(node.func)) {
|
|
1975
|
+
return true;
|
|
1976
|
+
}
|
|
1977
|
+
if (isExceptionName(node)) {
|
|
1978
|
+
return true;
|
|
1979
|
+
}
|
|
1980
|
+
// `x = __exception__` - CPython alias-clear artifact
|
|
1981
|
+
if (node instanceof AST.ASTStore && isExceptionName(node.src)) {
|
|
1982
|
+
return true;
|
|
1983
|
+
}
|
|
1984
|
+
// `return __exception__` - misrendered reraise epilogue
|
|
1985
|
+
if (node instanceof AST.ASTReturn && isExceptionName(node.value)) {
|
|
1986
|
+
return true;
|
|
1987
|
+
}
|
|
1988
|
+
return false;
|
|
1989
|
+
};
|
|
1990
|
+
|
|
1991
|
+
const rewriteContainer = (nodes, parentBlockType) => {
|
|
1992
|
+
if (!Array.isArray(nodes)) return;
|
|
1993
|
+
|
|
1994
|
+
// Pass 1: unwrap EXC_MATCH in Except block conditions.
|
|
1995
|
+
for (const node of nodes) {
|
|
1996
|
+
if (node instanceof AST.ASTCondBlock &&
|
|
1997
|
+
node.blockType === AST.ASTBlock.BlockType.Except) {
|
|
1998
|
+
node.condition = unwrapExcMatch(node.condition);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// Pass 2: convert ASTWhile/ASTIf with EXC_MATCH condition to Except
|
|
2003
|
+
// handlers and attach them to the preceding Try block. If no preceding
|
|
2004
|
+
// Try exists (deeply misparsed region), drop them to avoid invalid output.
|
|
2005
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2006
|
+
const node = nodes[i];
|
|
2007
|
+
if (!(node instanceof AST.ASTCondBlock) ||
|
|
2008
|
+
!(node.blockType === AST.ASTBlock.BlockType.While ||
|
|
2009
|
+
node.blockType === AST.ASTBlock.BlockType.If)) {
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
if (!isExcMatchCompare(node.condition) ||
|
|
2013
|
+
!containsExceptionRef(node.condition.left)) {
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
const matchType = node.condition.right;
|
|
2017
|
+
// Find nearest preceding Try block in this container.
|
|
2018
|
+
let tryIdx = -1;
|
|
2019
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
2020
|
+
const cand = nodes[j];
|
|
2021
|
+
if (cand instanceof AST.ASTBlock &&
|
|
2022
|
+
cand.blockType === AST.ASTBlock.BlockType.Try) {
|
|
2023
|
+
tryIdx = j;
|
|
2024
|
+
break;
|
|
2025
|
+
}
|
|
2026
|
+
// Allow existing Except handlers between; skip past them.
|
|
2027
|
+
if (cand instanceof AST.ASTCondBlock &&
|
|
2028
|
+
cand.blockType === AST.ASTBlock.BlockType.Except) {
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
// Strip any trailing orphan reraise/alias-clear statements from body.
|
|
2034
|
+
const cleanBody = node.nodes.filter(n => !isOrphanExceptionStmt(n));
|
|
2035
|
+
if (cleanBody.length === 0) {
|
|
2036
|
+
cleanBody.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
if (tryIdx >= 0) {
|
|
2040
|
+
const exceptBlk = new AST.ASTCondBlock(
|
|
2041
|
+
AST.ASTBlock.BlockType.Except,
|
|
2042
|
+
node.start, node.end, matchType, false);
|
|
2043
|
+
exceptBlk.line = node.line;
|
|
2044
|
+
exceptBlk.m_nodes = cleanBody;
|
|
2045
|
+
// Insert right after last existing Except handler for that Try.
|
|
2046
|
+
let insertAt = tryIdx + 1;
|
|
2047
|
+
while (insertAt < nodes.length &&
|
|
2048
|
+
nodes[insertAt] instanceof AST.ASTCondBlock &&
|
|
2049
|
+
nodes[insertAt].blockType === AST.ASTBlock.BlockType.Except) {
|
|
2050
|
+
insertAt++;
|
|
2051
|
+
}
|
|
2052
|
+
nodes.splice(i, 1);
|
|
2053
|
+
if (insertAt > i) insertAt--;
|
|
2054
|
+
nodes.splice(insertAt, 0, exceptBlk);
|
|
2055
|
+
} else {
|
|
2056
|
+
// No Try to attach to — dropping keeps output parseable.
|
|
2057
|
+
nodes.splice(i, 1);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// Pass 3: strip orphan exception-related statements at container
|
|
2062
|
+
// level — but NOT inside Except/Finally bodies, where bare `raise`
|
|
2063
|
+
// is a legitimate re-raise statement.
|
|
2064
|
+
const inHandlerBody = parentBlockType === AST.ASTBlock.BlockType.Except ||
|
|
2065
|
+
parentBlockType === AST.ASTBlock.BlockType.Finally;
|
|
2066
|
+
if (!inHandlerBody) {
|
|
2067
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2068
|
+
if (isOrphanExceptionStmt(nodes[i])) {
|
|
2069
|
+
nodes.splice(i, 1);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// Pass 3.5: strip `X = None; del X` pairs — CPython 3.11+ emits this
|
|
2075
|
+
// at the tail/fall-through of every `except Foo as X:` body to clear
|
|
2076
|
+
// the binding. The pattern is synthetic and user code effectively
|
|
2077
|
+
// never writes it literally, so removing it is safe and prevents
|
|
2078
|
+
// the artifact from breaking up adjacent except clauses.
|
|
2079
|
+
for (let i = nodes.length - 2; i >= 0; i--) {
|
|
2080
|
+
const a = nodes[i], b = nodes[i + 1];
|
|
2081
|
+
if (a instanceof AST.ASTStore &&
|
|
2082
|
+
a.src instanceof AST.ASTNone &&
|
|
2083
|
+
a.dest instanceof AST.ASTName &&
|
|
2084
|
+
b instanceof AST.ASTDelete &&
|
|
2085
|
+
b.value instanceof AST.ASTName &&
|
|
2086
|
+
a.dest.name === b.value.name) {
|
|
2087
|
+
nodes.splice(i, 2);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// Pass 4: drop empty `try:` blocks. With no following handler the
|
|
2092
|
+
// try is a bare leftover. With a trivial `except: pass` handler the
|
|
2093
|
+
// whole pair is a reconstruction artifact — Python forbids
|
|
2094
|
+
// `try:<empty>`, so either outcome requires the try to go. When the
|
|
2095
|
+
// handler has real body drop only the try and splice a `pass`
|
|
2096
|
+
// placeholder into the handler's matching — but the current
|
|
2097
|
+
// sightings all have pass-only handlers, so mirror that case.
|
|
2098
|
+
const isTrivialHandlerBlock = (n) =>
|
|
2099
|
+
n instanceof AST.ASTCondBlock &&
|
|
2100
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2101
|
+
n.blockType === AST.ASTBlock.BlockType.Finally) &&
|
|
2102
|
+
(n.nodes.length === 0 ||
|
|
2103
|
+
(n.nodes.length === 1 &&
|
|
2104
|
+
n.nodes[0] instanceof AST.ASTKeyword &&
|
|
2105
|
+
n.nodes[0].key === AST.ASTKeyword.Word.Pass));
|
|
2106
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2107
|
+
if (!isEmptyTryBlock(nodes[i])) continue;
|
|
2108
|
+
const next = nodes[i + 1];
|
|
2109
|
+
const hasHandler = next instanceof AST.ASTCondBlock &&
|
|
2110
|
+
(next.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2111
|
+
next.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2112
|
+
if (!hasHandler) {
|
|
2113
|
+
nodes.splice(i, 1);
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
// Walk all trailing handlers; drop the whole cluster iff every
|
|
2117
|
+
// one is a trivial pass-body — keeping any would still leave
|
|
2118
|
+
// the empty try as a syntax error anyway.
|
|
2119
|
+
let tailEnd = i + 1;
|
|
2120
|
+
while (tailEnd < nodes.length && isTrivialHandlerBlock(nodes[tailEnd])) {
|
|
2121
|
+
tailEnd++;
|
|
2122
|
+
}
|
|
2123
|
+
const allTrivial = tailEnd > i + 1 &&
|
|
2124
|
+
nodes.slice(i + 1, tailEnd).every(isTrivialHandlerBlock) &&
|
|
2125
|
+
(tailEnd === nodes.length ||
|
|
2126
|
+
!(nodes[tailEnd] instanceof AST.ASTCondBlock &&
|
|
2127
|
+
(nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Except ||
|
|
2128
|
+
nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Finally)));
|
|
2129
|
+
if (allTrivial) {
|
|
2130
|
+
nodes.splice(i, tailEnd - i);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// Pass 4.5: move a sibling Except/Finally that immediately follows
|
|
2135
|
+
// a Try into the Try's body. When CFG reconstruction hoists the
|
|
2136
|
+
// handler one level up (e.g. into the parent Else), the block
|
|
2137
|
+
// renderer dedents `except:` to the parent's indent — producing
|
|
2138
|
+
// `else: / try: / except:` where `except:` lands at the `else:`
|
|
2139
|
+
// column instead of the `try:` column, which is a SyntaxError.
|
|
2140
|
+
// Absorbing the handler into the Try body lets the renderer's
|
|
2141
|
+
// single-step dedent land it at the correct `try:` column.
|
|
2142
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
2143
|
+
const cur = nodes[i];
|
|
2144
|
+
if (!(cur instanceof AST.ASTBlock) ||
|
|
2145
|
+
cur.blockType !== AST.ASTBlock.BlockType.Try) continue;
|
|
2146
|
+
const next = nodes[i + 1];
|
|
2147
|
+
if (!(next instanceof AST.ASTCondBlock)) continue;
|
|
2148
|
+
if (next.blockType !== AST.ASTBlock.BlockType.Except &&
|
|
2149
|
+
next.blockType !== AST.ASTBlock.BlockType.Finally) continue;
|
|
2150
|
+
// Skip if the Try already carries its own trailing handler —
|
|
2151
|
+
// moving another one in would stack two handlers, which is
|
|
2152
|
+
// only valid for multi-except chains (same Try). Those cases
|
|
2153
|
+
// already render fine.
|
|
2154
|
+
const last = cur.nodes[cur.nodes.length - 1];
|
|
2155
|
+
const hasOwnHandler = last instanceof AST.ASTCondBlock &&
|
|
2156
|
+
(last.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2157
|
+
last.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2158
|
+
if (hasOwnHandler) continue;
|
|
2159
|
+
// Empty Try bodies + non-trivial handler would collapse to an
|
|
2160
|
+
// invalid `try: / except:` with nothing between. Inject a
|
|
2161
|
+
// `pass` so the rendered block is syntactically valid.
|
|
2162
|
+
if (cur.nodes.length === 0) {
|
|
2163
|
+
cur.nodes.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
2164
|
+
}
|
|
2165
|
+
cur.nodes.push(next);
|
|
2166
|
+
nodes.splice(i + 1, 1);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// Pass 5: a Try block with a non-empty body but no following
|
|
2170
|
+
// Except/Finally handler is a SyntaxError. Append a synthetic
|
|
2171
|
+
// `except: pass` so the output parses — real structural recovery
|
|
2172
|
+
// needs dedicated work, but the fallback keeps the file usable.
|
|
2173
|
+
// Skip when the Try already has an Except/Finally nested in its
|
|
2174
|
+
// body (except* groups build nested handler structures).
|
|
2175
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
2176
|
+
const cur = nodes[i];
|
|
2177
|
+
if (!(cur instanceof AST.ASTBlock) ||
|
|
2178
|
+
cur.blockType !== AST.ASTBlock.BlockType.Try) {
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
if (cur.nodes.length === 0) continue;
|
|
2182
|
+
const next = nodes[i + 1];
|
|
2183
|
+
const hasHandler = next instanceof AST.ASTCondBlock &&
|
|
2184
|
+
(next.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2185
|
+
next.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2186
|
+
if (hasHandler) continue;
|
|
2187
|
+
const hasNestedExcept = cur.nodes.some(n =>
|
|
2188
|
+
n instanceof AST.ASTCondBlock &&
|
|
2189
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2190
|
+
n.blockType === AST.ASTBlock.BlockType.Finally));
|
|
2191
|
+
if (hasNestedExcept) continue;
|
|
2192
|
+
const synthetic = new AST.ASTCondBlock(
|
|
2193
|
+
AST.ASTBlock.BlockType.Except,
|
|
2194
|
+
cur.end, cur.end, null, false);
|
|
2195
|
+
synthetic.line = cur.line;
|
|
2196
|
+
synthetic.m_nodes.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
2197
|
+
nodes.splice(i + 1, 0, synthetic);
|
|
2198
|
+
i++;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// Pass 5.5: adopt orphan Except into a preceding branch whose
|
|
2202
|
+
// body ends with an un-paired Try. CPython can emit a try/except
|
|
2203
|
+
// inside the tail of an if/else branch so that, after CFG
|
|
2204
|
+
// reconstruction, the Try ends up as the last child of the branch
|
|
2205
|
+
// while its handler gets hoisted one level up as a stray sibling.
|
|
2206
|
+
// Moving the handler back in keeps the pair valid. Absorbed
|
|
2207
|
+
// trailing statements stay in the handler body — not semantically
|
|
2208
|
+
// correct, but renders to parseable Python instead of a stray
|
|
2209
|
+
// `except:` at the outer indent.
|
|
2210
|
+
const branchTypes = [
|
|
2211
|
+
AST.ASTBlock.BlockType.If,
|
|
2212
|
+
AST.ASTBlock.BlockType.Elif,
|
|
2213
|
+
AST.ASTBlock.BlockType.Else,
|
|
2214
|
+
];
|
|
2215
|
+
const endsWithOrphanTry = (block) => {
|
|
2216
|
+
if (!(block instanceof AST.ASTBlock)) return false;
|
|
2217
|
+
if (!branchTypes.includes(block.blockType)) return false;
|
|
2218
|
+
const body = block.nodes;
|
|
2219
|
+
if (!body || body.length === 0) return false;
|
|
2220
|
+
const last = body[body.length - 1];
|
|
2221
|
+
if (!(last instanceof AST.ASTBlock) ||
|
|
2222
|
+
last.blockType !== AST.ASTBlock.BlockType.Try) {
|
|
2223
|
+
return false;
|
|
2224
|
+
}
|
|
2225
|
+
const hasHandlerInside = last.nodes.some(n =>
|
|
2226
|
+
n instanceof AST.ASTCondBlock &&
|
|
2227
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2228
|
+
n.blockType === AST.ASTBlock.BlockType.Finally));
|
|
2229
|
+
return !hasHandlerInside;
|
|
2230
|
+
};
|
|
2231
|
+
for (let i = nodes.length - 1; i > 0; i--) {
|
|
2232
|
+
const cur = nodes[i];
|
|
2233
|
+
if (!(cur instanceof AST.ASTCondBlock)) continue;
|
|
2234
|
+
if (cur.blockType !== AST.ASTBlock.BlockType.Except &&
|
|
2235
|
+
cur.blockType !== AST.ASTBlock.BlockType.Finally) continue;
|
|
2236
|
+
const prev = nodes[i - 1];
|
|
2237
|
+
if (!endsWithOrphanTry(prev)) continue;
|
|
2238
|
+
// Confirm there's no matching Try at this container level,
|
|
2239
|
+
// otherwise leave the pair alone.
|
|
2240
|
+
let hasLocalTry = false;
|
|
2241
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
2242
|
+
const p = nodes[j];
|
|
2243
|
+
if (p instanceof AST.ASTBlock &&
|
|
2244
|
+
p.blockType === AST.ASTBlock.BlockType.Try) {
|
|
2245
|
+
hasLocalTry = true;
|
|
2246
|
+
break;
|
|
2247
|
+
}
|
|
2248
|
+
if (p instanceof AST.ASTCondBlock &&
|
|
2249
|
+
(p.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2250
|
+
p.blockType === AST.ASTBlock.BlockType.Finally)) {
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
if (hasLocalTry) continue;
|
|
2256
|
+
prev.nodes.push(cur);
|
|
2257
|
+
nodes.splice(i, 1);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// Pass 6: drop orphan Except/Finally blocks that have no matching
|
|
2261
|
+
// Try (or preceding Except) anywhere earlier in this container.
|
|
2262
|
+
// These are leftovers from misreconstructed CFGs — keeping them
|
|
2263
|
+
// produces SyntaxError at parse time. Inside a Try body Except
|
|
2264
|
+
// can legitimately nest (Py3.11+ handler layout, except* groups),
|
|
2265
|
+
// but only at the tail. A mid-body Except with non-handler
|
|
2266
|
+
// statements after it is a stray handler — drop it.
|
|
2267
|
+
const inTryBody = parentBlockType === AST.ASTBlock.BlockType.Try;
|
|
2268
|
+
const isHandlerBlock = (x) =>
|
|
2269
|
+
x instanceof AST.ASTCondBlock &&
|
|
2270
|
+
(x.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2271
|
+
x.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2272
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2273
|
+
const cur = nodes[i];
|
|
2274
|
+
if (!(cur instanceof AST.ASTCondBlock)) continue;
|
|
2275
|
+
if (cur.blockType !== AST.ASTBlock.BlockType.Except &&
|
|
2276
|
+
cur.blockType !== AST.ASTBlock.BlockType.Finally) continue;
|
|
2277
|
+
if (inTryBody) {
|
|
2278
|
+
// except* groups have complex internal structure — never
|
|
2279
|
+
// touch them.
|
|
2280
|
+
if (cur.isExceptStar) continue;
|
|
2281
|
+
// For regular Except nested inside Try body (Py3.11+
|
|
2282
|
+
// handler layout): legitimate only when at the tail.
|
|
2283
|
+
let allTrailingHandlers = true;
|
|
2284
|
+
for (let k = i + 1; k < nodes.length; k++) {
|
|
2285
|
+
if (!isHandlerBlock(nodes[k])) {
|
|
2286
|
+
allTrailingHandlers = false;
|
|
2287
|
+
break;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if (allTrailingHandlers) continue;
|
|
2291
|
+
}
|
|
2292
|
+
let anchorFound = false;
|
|
2293
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
2294
|
+
const prev = nodes[j];
|
|
2295
|
+
if (prev instanceof AST.ASTBlock &&
|
|
2296
|
+
prev.blockType === AST.ASTBlock.BlockType.Try) {
|
|
2297
|
+
anchorFound = true;
|
|
2298
|
+
break;
|
|
2299
|
+
}
|
|
2300
|
+
if (prev instanceof AST.ASTCondBlock &&
|
|
2301
|
+
(prev.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2302
|
+
prev.blockType === AST.ASTBlock.BlockType.Finally)) {
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2307
|
+
if (!anchorFound) nodes.splice(i, 1);
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// Pass 7: after earlier passes may have emptied a Try body, drop
|
|
2311
|
+
// Try+trivial-Except pairs where the body is now empty. Python
|
|
2312
|
+
// forbids `try:` with no body; a pass-only handler carries no
|
|
2313
|
+
// recoverable intent, so splice the cluster out entirely.
|
|
2314
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2315
|
+
if (!isEmptyTryBlock(nodes[i])) continue;
|
|
2316
|
+
let tailEnd = i + 1;
|
|
2317
|
+
while (tailEnd < nodes.length && isTrivialHandlerBlock(nodes[tailEnd])) {
|
|
2318
|
+
tailEnd++;
|
|
2319
|
+
}
|
|
2320
|
+
if (tailEnd === i + 1) continue;
|
|
2321
|
+
const trailingHandler = nodes[tailEnd] instanceof AST.ASTCondBlock &&
|
|
2322
|
+
(nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Except ||
|
|
2323
|
+
nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Finally);
|
|
2324
|
+
if (trailingHandler) continue;
|
|
2325
|
+
nodes.splice(i, tailEnd - i);
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2329
|
+
// Post-order traversal: process children before parent so nested
|
|
2330
|
+
// emptyings bubble up — an outer Try can only be recognised as empty
|
|
2331
|
+
// after the inner Try/Except artifacts inside its body were dropped.
|
|
2332
|
+
const visited = new WeakSet();
|
|
2333
|
+
const visit = (n) => {
|
|
2334
|
+
if (!n || visited.has(n)) return;
|
|
2335
|
+
visited.add(n);
|
|
2336
|
+
let nodes = null;
|
|
2337
|
+
let parentType = null;
|
|
2338
|
+
if (n instanceof AST.ASTNodeList) {
|
|
2339
|
+
nodes = n.list;
|
|
2340
|
+
parentType = null;
|
|
2341
|
+
} else if (n instanceof AST.ASTBlock) {
|
|
2342
|
+
nodes = n.nodes;
|
|
2343
|
+
parentType = n.blockType;
|
|
2344
|
+
} else if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2345
|
+
const body = n.src.code?.object?.SourceCode;
|
|
2346
|
+
if (body) visit(body);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
if (nodes) {
|
|
2350
|
+
for (const c of nodes) visit(c);
|
|
2351
|
+
rewriteContainer(nodes, parentType);
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
visit(root);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// An Except block can get reconstructed inside a non-Try block's body
|
|
2358
|
+
// (most often a For/While/If inside the protected try-body) when the
|
|
2359
|
+
// handler's offset range overlaps a nested loop. Python requires the
|
|
2360
|
+
// except clause to sit next to its `try:`, so walk the tree and pull
|
|
2361
|
+
// such Except blocks out of any non-Try ancestor until they become a
|
|
2362
|
+
// sibling of their enclosing Try.
|
|
2363
|
+
hoistNestedExceptBlocks(root) {
|
|
2364
|
+
const isHoistableBlock = (node) =>
|
|
2365
|
+
node instanceof AST.ASTBlock &&
|
|
2366
|
+
node.blockType !== AST.ASTBlock.BlockType.Try;
|
|
2367
|
+
|
|
2368
|
+
const isExcept = (node) =>
|
|
2369
|
+
node instanceof AST.ASTCondBlock &&
|
|
2370
|
+
node.blockType === AST.ASTBlock.BlockType.Except;
|
|
2371
|
+
|
|
2372
|
+
// Fixed-point loop: one pass hoists Except up one level. Bounded by
|
|
2373
|
+
// tree depth; excessive iterations just exit.
|
|
2374
|
+
for (let iter = 0; iter < 64; iter++) {
|
|
2375
|
+
let moved = false;
|
|
2376
|
+
const stack = [{container: null, nodes: null, parent: null}];
|
|
2377
|
+
const parentMap = new WeakMap(); // child container → {arr, index, parent}
|
|
2378
|
+
|
|
2379
|
+
const visit = (arr, parentInfo) => {
|
|
2380
|
+
for (let i = 0; i < arr.length; i++) {
|
|
2381
|
+
const n = arr[i];
|
|
2382
|
+
if (!n) continue;
|
|
2383
|
+
let children = null;
|
|
2384
|
+
if (n instanceof AST.ASTBlock || n instanceof AST.ASTCondBlock) {
|
|
2385
|
+
children = n.nodes;
|
|
2386
|
+
} else if (n instanceof AST.ASTNodeList) {
|
|
2387
|
+
children = n.list;
|
|
2388
|
+
} else if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2389
|
+
const src = n.src.code?.object?.SourceCode;
|
|
2390
|
+
if (src instanceof AST.ASTNodeList) {
|
|
2391
|
+
visit(src.list, null);
|
|
2392
|
+
}
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
2396
|
+
// If this block is non-Try and its first child is an
|
|
2397
|
+
// Except, pull that Except out into the parent arr
|
|
2398
|
+
// right after `n`.
|
|
2399
|
+
if (isHoistableBlock(n)) {
|
|
2400
|
+
// Scan forward for leading Except children only;
|
|
2401
|
+
// an Except appearing later in the body is usually
|
|
2402
|
+
// a real construct we should leave alone.
|
|
2403
|
+
while (children.length > 0 && isExcept(children[0])) {
|
|
2404
|
+
const ex = children.shift();
|
|
2405
|
+
arr.splice(i + 1, 0, ex);
|
|
2406
|
+
moved = true;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
visit(children, {arr, index: i});
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2414
|
+
const roots = [];
|
|
2415
|
+
if (root instanceof AST.ASTNodeList) roots.push(root.list);
|
|
2416
|
+
else if (root instanceof AST.ASTBlock) roots.push(root.nodes);
|
|
2417
|
+
|
|
2418
|
+
for (const r of roots) visit(r, null);
|
|
2419
|
+
|
|
2420
|
+
// Also dive into nested function bodies.
|
|
2421
|
+
const walk = (n, seen) => {
|
|
2422
|
+
if (!n || seen.has(n)) return;
|
|
2423
|
+
seen.add(n);
|
|
2424
|
+
if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2425
|
+
const src = n.src.code?.object?.SourceCode;
|
|
2426
|
+
if (src instanceof AST.ASTNodeList) {
|
|
2427
|
+
visit(src.list, null);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
let children = null;
|
|
2431
|
+
if (n instanceof AST.ASTNodeList) children = n.list;
|
|
2432
|
+
else if (n instanceof AST.ASTBlock || n instanceof AST.ASTCondBlock) children = n.nodes;
|
|
2433
|
+
if (Array.isArray(children)) {
|
|
2434
|
+
for (const c of children) walk(c, seen);
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
walk(root, new WeakSet());
|
|
2438
|
+
|
|
2439
|
+
if (!moved) break;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
1804
2443
|
dedupeExceptHandlers(root) {
|
|
1805
2444
|
const queue = [root];
|
|
1806
2445
|
const visited = new WeakSet();
|
|
@@ -2085,6 +2724,14 @@ class PycDecompiler {
|
|
|
2085
2724
|
const frag = ch?.codeFragment?.()?.toString?.() || "";
|
|
2086
2725
|
return frag === "pass" || frag.includes("__exception__") || frag.includes("##ERROR##");
|
|
2087
2726
|
}))) {
|
|
2727
|
+
// Drop only synthetic except* handlers without a user condition.
|
|
2728
|
+
// User-written `except:` / `except E:` must be preserved; replace
|
|
2729
|
+
// cleanup-only body with `pass` so the try statement stays valid.
|
|
2730
|
+
if (node.isExceptStar && !node.condition) {
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
node.m_nodes = [new AST.ASTKeyword(AST.ASTKeyword.Word.Pass)];
|
|
2734
|
+
rewritten.push(node);
|
|
2088
2735
|
continue;
|
|
2089
2736
|
}
|
|
2090
2737
|
if (typeof node?.codeFragment === 'function') {
|
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("'")) {
|
package/lib/ast/ast_node.js
CHANGED
|
@@ -258,7 +258,19 @@ class ASTNodeList extends ASTNode {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
|
|
261
|
+
// Only join with `; ` between two simple (non-compound) statements
|
|
262
|
+
// that also happen to be on the same source line. A compound block
|
|
263
|
+
// (if/while/for/try/function/class) must never be semicolon-joined
|
|
264
|
+
// to a following sibling — doing so swallows the sibling into the
|
|
265
|
+
// block's body and breaks structure.
|
|
266
|
+
const prevIsCompound = prevNode instanceof ASTBlock ||
|
|
267
|
+
prevNode instanceof ASTFunction ||
|
|
268
|
+
prevNode instanceof ASTClass;
|
|
269
|
+
const nodeIsCompound = node instanceof ASTBlock ||
|
|
270
|
+
node instanceof ASTFunction ||
|
|
271
|
+
node instanceof ASTClass;
|
|
272
|
+
if (prevNode && spacing == 0 && sourceFragment.length == 1 &&
|
|
273
|
+
!prevIsCompound && !nodeIsCompound) {
|
|
262
274
|
result.lastLineAppend((prevNode ? "; " : "") + sourceFragment.toString(), false);
|
|
263
275
|
} else {
|
|
264
276
|
const blankCount = rawSpacing ? Math.max(0, spacing - 1) : (spacing > 1 ? 1 : 0);
|
|
@@ -465,7 +477,11 @@ class ASTObject extends ASTNode {
|
|
|
465
477
|
.replace(/\\/g, '\\\\')
|
|
466
478
|
.replace(/\n/g, '\\n')
|
|
467
479
|
.replace(/\r/g, '\\r')
|
|
468
|
-
.replace(/\t/g, '\\t')
|
|
480
|
+
.replace(/\t/g, '\\t')
|
|
481
|
+
// Any remaining C0/DEL byte must be hex-escaped; leaving a raw
|
|
482
|
+
// NUL or BEL in the source makes the output unparseable.
|
|
483
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, c =>
|
|
484
|
+
'\\x' + c.charCodeAt(0).toString(16).padStart(2, '0'));
|
|
469
485
|
let quote = '"';
|
|
470
486
|
if (escaped.includes('"')) {
|
|
471
487
|
if (!escaped.includes("'")) {
|
|
@@ -1103,7 +1119,13 @@ class ASTStore extends ASTNode {
|
|
|
1103
1119
|
if (inLambda) {
|
|
1104
1120
|
result.lastLineAppend(methodBody);
|
|
1105
1121
|
} else {
|
|
1106
|
-
|
|
1122
|
+
const bodyHasContent = methodBody && methodBody.toString().trim().length;
|
|
1123
|
+
const hasGlobals = codeObject.Globals && codeObject.Globals.size > 0;
|
|
1124
|
+
if (bodyHasContent) {
|
|
1125
|
+
result.add(methodBody);
|
|
1126
|
+
} else if (!hasGlobals) {
|
|
1127
|
+
result.add("pass");
|
|
1128
|
+
}
|
|
1107
1129
|
}
|
|
1108
1130
|
result.decreaseIndent();
|
|
1109
1131
|
|
|
@@ -2539,19 +2561,33 @@ class ASTBlock extends ASTNode {
|
|
|
2539
2561
|
result.add("pass");
|
|
2540
2562
|
}
|
|
2541
2563
|
} else {
|
|
2564
|
+
// Emit body nodes first, then trailing dedented handlers. If
|
|
2565
|
+
// every body node was `skip`-ed (e.g. Try body whose sole
|
|
2566
|
+
// child was consumed by ternary collapse), the block would
|
|
2567
|
+
// print `try:` with no body — inject a `pass` placeholder so
|
|
2568
|
+
// the resulting source stays parseable.
|
|
2569
|
+
let emittedBodyNode = false;
|
|
2570
|
+
const handlerTail = [];
|
|
2542
2571
|
renderNodes.map(node => {
|
|
2543
2572
|
if (!node || node.skip) {
|
|
2544
2573
|
return;
|
|
2545
2574
|
}
|
|
2546
2575
|
const isHandler = node.blockType == ASTBlock.BlockType.Except || node.blockType == ASTBlock.BlockType.Finally;
|
|
2547
2576
|
if (isHandler) {
|
|
2548
|
-
|
|
2549
|
-
result.add(node.codeFragment());
|
|
2550
|
-
result.increaseIndent();
|
|
2577
|
+
handlerTail.push(node);
|
|
2551
2578
|
} else {
|
|
2552
2579
|
result.add(node.codeFragment());
|
|
2580
|
+
emittedBodyNode = true;
|
|
2553
2581
|
}
|
|
2554
2582
|
});
|
|
2583
|
+
if (!emittedBodyNode) {
|
|
2584
|
+
result.add("pass");
|
|
2585
|
+
}
|
|
2586
|
+
for (const h of handlerTail) {
|
|
2587
|
+
result.decreaseIndent();
|
|
2588
|
+
result.add(h.codeFragment());
|
|
2589
|
+
result.increaseIndent();
|
|
2590
|
+
}
|
|
2555
2591
|
}
|
|
2556
2592
|
} else {
|
|
2557
2593
|
result.add("pass");
|
|
@@ -3137,12 +3173,27 @@ class ASTFormattedValue extends ASTNode {
|
|
|
3137
3173
|
return this.m_format_spec;
|
|
3138
3174
|
}
|
|
3139
3175
|
|
|
3140
|
-
codeFragment() {
|
|
3176
|
+
codeFragment(outerQuote = null) {
|
|
3141
3177
|
// Format: {value} or {value!r} or {value:.2f}
|
|
3178
|
+
// `outerQuote` is set only when this FormattedValue is being rendered
|
|
3179
|
+
// inside an enclosing f-string; nested strings must then use the
|
|
3180
|
+
// opposite delimiter so the outer string does not close prematurely.
|
|
3181
|
+
const innerQuote = outerQuote === '"' ? "'" :
|
|
3182
|
+
outerQuote === "'" ? '"' : '"';
|
|
3142
3183
|
let result = "{";
|
|
3143
3184
|
|
|
3144
3185
|
if (this.m_val) {
|
|
3145
|
-
|
|
3186
|
+
if (this.m_val instanceof ASTJoinedStr) {
|
|
3187
|
+
// Only force a flipped quote when we actually have an outer
|
|
3188
|
+
// f-string; otherwise let the nested string keep its default.
|
|
3189
|
+
if (outerQuote) {
|
|
3190
|
+
result += this.m_val.codeFragment(innerQuote);
|
|
3191
|
+
} else {
|
|
3192
|
+
result += this.m_val.codeFragment();
|
|
3193
|
+
}
|
|
3194
|
+
} else {
|
|
3195
|
+
result += this.m_val.codeFragment();
|
|
3196
|
+
}
|
|
3146
3197
|
}
|
|
3147
3198
|
|
|
3148
3199
|
// Add conversion flag (!s, !r, !a)
|
|
@@ -3163,7 +3214,10 @@ class ASTFormattedValue extends ASTNode {
|
|
|
3163
3214
|
result += ":";
|
|
3164
3215
|
// Format spec can be ASTJoinedStr (nested f-string) or string constant
|
|
3165
3216
|
if (this.m_format_spec instanceof ASTJoinedStr) {
|
|
3166
|
-
|
|
3217
|
+
// Format-spec content is already inside the outer f-string's
|
|
3218
|
+
// braces, so its literal parts must not use the outer quote.
|
|
3219
|
+
const specQuote = outerQuote ? innerQuote : '"';
|
|
3220
|
+
result += this.m_format_spec.codeFragment(specQuote, true);
|
|
3167
3221
|
} else if (this.m_format_spec instanceof ASTObject) {
|
|
3168
3222
|
// String constant like ".2f"
|
|
3169
3223
|
result += this.m_format_spec.object.Value;
|
|
@@ -3196,9 +3250,14 @@ class ASTJoinedStr extends ASTNode {
|
|
|
3196
3250
|
get lastLine() {
|
|
3197
3251
|
return this.values[this.values.length - 1]?.lastLine;
|
|
3198
3252
|
}
|
|
3199
|
-
codeFragment() {
|
|
3253
|
+
codeFragment(quoteChar = '"', bareInnerForFormatSpec = false) {
|
|
3200
3254
|
// PEP 750 t-strings use t"..." prefix; otherwise f-string.
|
|
3201
|
-
|
|
3255
|
+
// `quoteChar` lets nested f-strings pick the opposite delimiter so
|
|
3256
|
+
// they don't collide with the enclosing string. `bareInnerForFormatSpec`
|
|
3257
|
+
// skips the f"..." wrapper entirely (used when this ASTJoinedStr is a
|
|
3258
|
+
// format spec nested inside {...}).
|
|
3259
|
+
const prefix = this.isTemplateString ? 't' : 'f';
|
|
3260
|
+
let result = bareInnerForFormatSpec ? "" : `${prefix}${quoteChar}`;
|
|
3202
3261
|
|
|
3203
3262
|
// Values are in reverse order (BUILD_STRING pops from stack)
|
|
3204
3263
|
// So we need to reverse them
|
|
@@ -3232,8 +3291,10 @@ class ASTJoinedStr extends ASTNode {
|
|
|
3232
3291
|
let prefix = prevStr.substring(0, prevStr.length - match[0].length);
|
|
3233
3292
|
|
|
3234
3293
|
// Remove the previously added literal and replace with prefix + {var=}
|
|
3294
|
+
let quoteEsc = quoteChar === '"' ? /"/g : /'/g;
|
|
3295
|
+
let quoteReplace = quoteChar === '"' ? '\\"' : "\\'";
|
|
3235
3296
|
let beforeLiteral = result.lastIndexOf(prevStr.replace(/\\/g, '\\\\')
|
|
3236
|
-
.replace(
|
|
3297
|
+
.replace(quoteEsc, quoteReplace).replace(/\n/g, '\\n').replace(/\t/g, '\\t'));
|
|
3237
3298
|
|
|
3238
3299
|
if (beforeLiteral !== -1) {
|
|
3239
3300
|
result = result.substring(0, beforeLiteral);
|
|
@@ -3242,7 +3303,7 @@ class ASTJoinedStr extends ASTNode {
|
|
|
3242
3303
|
// Add prefix (escaped) if present
|
|
3243
3304
|
if (prefix) {
|
|
3244
3305
|
let escapedPrefix = prefix.replace(/\\/g, '\\\\');
|
|
3245
|
-
escapedPrefix = escapedPrefix.replace(
|
|
3306
|
+
escapedPrefix = escapedPrefix.replace(quoteEsc, quoteReplace);
|
|
3246
3307
|
escapedPrefix = escapedPrefix.replace(/\n/g, '\\n');
|
|
3247
3308
|
escapedPrefix = escapedPrefix.replace(/\t/g, '\\t');
|
|
3248
3309
|
result += escapedPrefix;
|
|
@@ -3254,16 +3315,25 @@ class ASTJoinedStr extends ASTNode {
|
|
|
3254
3315
|
}
|
|
3255
3316
|
}
|
|
3256
3317
|
|
|
3257
|
-
// {expression} part
|
|
3258
|
-
|
|
3259
|
-
|
|
3318
|
+
// {expression} part - pass our own quote char so nested
|
|
3319
|
+
// f-strings / strings inside pick a compatible delimiter.
|
|
3320
|
+
result += value.codeFragment(quoteChar);
|
|
3321
|
+
// result above calls ASTFormattedValue.codeFragment(outerQuote).
|
|
3322
|
+
} else if (value instanceof ASTObject && ['Py_String', 'Py_Unicode'].includes(value.object?.ClassName)) {
|
|
3260
3323
|
// Literal string part - need to escape special chars
|
|
3261
3324
|
let str = value.object.Value;
|
|
3262
|
-
// Escape backslashes and
|
|
3325
|
+
// Escape backslashes and the current quote char
|
|
3263
3326
|
str = str.replace(/\\/g, '\\\\');
|
|
3264
|
-
|
|
3327
|
+
if (quoteChar === '"') {
|
|
3328
|
+
str = str.replace(/"/g, '\\"');
|
|
3329
|
+
} else {
|
|
3330
|
+
str = str.replace(/'/g, "\\'");
|
|
3331
|
+
}
|
|
3265
3332
|
str = str.replace(/\n/g, '\\n');
|
|
3333
|
+
str = str.replace(/\r/g, '\\r');
|
|
3266
3334
|
str = str.replace(/\t/g, '\\t');
|
|
3335
|
+
str = str.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, c =>
|
|
3336
|
+
'\\x' + c.charCodeAt(0).toString(16).padStart(2, '0'));
|
|
3267
3337
|
result += str;
|
|
3268
3338
|
} else {
|
|
3269
3339
|
// Fallback for unexpected types
|
|
@@ -3271,7 +3341,9 @@ class ASTJoinedStr extends ASTNode {
|
|
|
3271
3341
|
}
|
|
3272
3342
|
}
|
|
3273
3343
|
|
|
3274
|
-
|
|
3344
|
+
if (!bareInnerForFormatSpec) {
|
|
3345
|
+
result += quoteChar;
|
|
3346
|
+
}
|
|
3275
3347
|
return result;
|
|
3276
3348
|
}
|
|
3277
3349
|
|
|
@@ -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);
|
|
@@ -582,7 +582,10 @@ function hasUpcomingStringBuild(context) {
|
|
|
582
582
|
context.OpCodes.STORE_DEREF_A
|
|
583
583
|
]);
|
|
584
584
|
|
|
585
|
-
|
|
585
|
+
// 3.11+ inline caches inflate the physical instruction stream; a single
|
|
586
|
+
// f-string with a few interpolations easily spans 30+ code slots before
|
|
587
|
+
// reaching BUILD_STRING. Cap generously and rely on earlyStops to bound.
|
|
588
|
+
for (let step = 1; step <= 48; step++) {
|
|
586
589
|
const instr = context.code.PeekInstructionAtOffset(context.code.Current.Offset + step * 2);
|
|
587
590
|
if (!instr) {
|
|
588
591
|
break;
|
|
@@ -745,6 +748,28 @@ function handlePopTop() {
|
|
|
745
748
|
return;
|
|
746
749
|
}
|
|
747
750
|
|
|
751
|
+
// Mid-UNPACK_SEQUENCE POP_TOP: CPython emits `a, _, b = tup` as
|
|
752
|
+
// UNPACK 3 / STORE a / POP_TOP / STORE b, where POP_TOP drops the
|
|
753
|
+
// middle slot. Our unpack-state-machine accumulates STORE targets
|
|
754
|
+
// into a tuple; treat POP_TOP as an anonymous `_` slot so the
|
|
755
|
+
// remaining STORE_FAST still sees the tuple on the stack.
|
|
756
|
+
if (this.unpack > 0) {
|
|
757
|
+
let tupleNode = this.dataStack.top();
|
|
758
|
+
if (tupleNode instanceof AST.ASTTuple) {
|
|
759
|
+
let placeholder = new AST.ASTName("_");
|
|
760
|
+
if (this.starPos-- == 0) placeholder.name = "*_";
|
|
761
|
+
tupleNode.add(placeholder);
|
|
762
|
+
if (--this.unpack <= 0) {
|
|
763
|
+
this.dataStack.pop();
|
|
764
|
+
let seqNode = this.dataStack.pop();
|
|
765
|
+
let node = new AST.ASTStore(seqNode, tupleNode);
|
|
766
|
+
node.line = this.code.Current.LineNo;
|
|
767
|
+
this.curBlock.append(node);
|
|
768
|
+
}
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
748
773
|
if ([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)) {
|
|
749
774
|
if (this.curBlock.blockType == AST.ASTBlock.BlockType.Except) {
|
|
750
775
|
// Skipping POP_TOP, POP_TOP, POP_TOP
|
|
@@ -1111,7 +1136,15 @@ function handleReturnConstA() {
|
|
|
1111
1136
|
nextOp.OpCodeID === this.OpCodes.RERAISE;
|
|
1112
1137
|
const prevIsPOPExcept = this.code.Prev?.OpCodeID === this.OpCodes.POP_EXCEPT;
|
|
1113
1138
|
const nextIsSWAP = nextOp?.OpCodeID === this.OpCodes.SWAP_A;
|
|
1139
|
+
const nextIsRERAISE = nextOp?.OpCodeID === this.OpCodes.RERAISE ||
|
|
1140
|
+
nextOp?.OpCodeID === this.OpCodes.RERAISE_A;
|
|
1114
1141
|
const isExceptCleanup = isNone && prevIsPOPExcept && nextIsSWAP;
|
|
1142
|
+
// CPython 3.11+ places the function's implicit `return None` at the tail of an
|
|
1143
|
+
// except handler (POP_EXCEPT → RETURN_CONST None → RERAISE or end-of-function).
|
|
1144
|
+
// The source has no explicit return there; drop it so the handler body doesn't
|
|
1145
|
+
// inherit a phantom return.
|
|
1146
|
+
const isExceptTailImplicitReturn = isNone && prevIsPOPExcept &&
|
|
1147
|
+
(nextIsRERAISE || !nextOp);
|
|
1115
1148
|
|
|
1116
1149
|
if (isNone && isModuleLevel && isAtEnd) {
|
|
1117
1150
|
return; // Skip implicit return None at module end
|
|
@@ -1119,6 +1152,9 @@ function handleReturnConstA() {
|
|
|
1119
1152
|
if (isExceptCleanup) {
|
|
1120
1153
|
return; // Skip return None in exception cleanup (except* success path)
|
|
1121
1154
|
}
|
|
1155
|
+
if (isExceptTailImplicitReturn) {
|
|
1156
|
+
return; // Skip synthetic return None at end of except handler
|
|
1157
|
+
}
|
|
1122
1158
|
|
|
1123
1159
|
let value = new AST.ASTObject(this.code.Current.ConstantObject);
|
|
1124
1160
|
let node = new AST.ASTReturn(value);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "depyo",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Python bytecode decompiler (Python 1.0–3.
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Python bytecode decompiler (Python 1.0–3.15) implemented in Node.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"depyo": "./depyo.js"
|
|
7
7
|
},
|