depyo 1.0.2 → 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.
@@ -7,6 +7,19 @@ function indent(level = 1) {
7
7
  return Buffer.alloc(level * SPACES_PER_LEVEL, ' ').toString('ascii');
8
8
  }
9
9
 
10
+ function renderTypeParam(tp) {
11
+ // PEP 696: a typeparam may be {name, default: ASTNode} instead of a string.
12
+ if (tp && typeof tp === 'object' && tp.name && tp.default != null && !tp.codeFragment) {
13
+ const defFrag = tp.default?.codeFragment
14
+ ? tp.default.codeFragment()
15
+ : tp.default?.toString?.() ?? String(tp.default);
16
+ return `${tp.name} = ${defFrag}`;
17
+ }
18
+ if (tp?.codeFragment) return tp.codeFragment();
19
+ if (tp?.toString) return tp.toString();
20
+ return tp;
21
+ }
22
+
10
23
  class ASTNode {
11
24
 
12
25
  m_lineNo = -1;
@@ -51,11 +64,7 @@ class ASTNode {
51
64
  }
52
65
 
53
66
  codeFragment() {
54
- let result = `#TODO ${this.constructor.name}`;
55
- if (result === null || result === undefined) {
56
- return `${result}`;
57
- }
58
- return result;
67
+ throw new Error(`${this.constructor.name}.codeFragment() not implemented — subclass must override`);
59
68
  }
60
69
 
61
70
  static calculateSpacing(prevNode, node) {
@@ -249,7 +258,19 @@ class ASTNodeList extends ASTNode {
249
258
  }
250
259
  }
251
260
 
252
- if (prevNode && spacing == 0 && sourceFragment.length == 1) {
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) {
253
274
  result.lastLineAppend((prevNode ? "; " : "") + sourceFragment.toString(), false);
254
275
  } else {
255
276
  const blankCount = rawSpacing ? Math.max(0, spacing - 1) : (spacing > 1 ? 1 : 0);
@@ -446,19 +467,33 @@ class ASTObject extends ASTNode {
446
467
  }
447
468
  }
448
469
 
449
- let result = this.object.toString();
450
470
  if (["Py_String", "Py_Unicode"].includes(this.object.ClassName)) {
471
+ let raw = this.object.Value;
472
+ if (raw == null || raw.length === 0) {
473
+ return '""';
474
+ }
475
+ raw = raw.toString();
476
+ let escaped = raw
477
+ .replace(/\\/g, '\\\\')
478
+ .replace(/\n/g, '\\n')
479
+ .replace(/\r/g, '\\r')
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'));
451
485
  let quote = '"';
452
- if (result.includes('"')) {
453
- if (!result.includes("'")) {
486
+ if (escaped.includes('"')) {
487
+ if (!escaped.includes("'")) {
454
488
  quote = "'";
455
489
  } else {
456
- quote = '"';
457
- result = result.replace(/"/g, '\\"');
490
+ escaped = escaped.replace(/"/g, '\\"');
458
491
  }
459
492
  }
460
- return quote + result + quote;
493
+ return quote + escaped + quote;
461
494
  }
495
+
496
+ let result = this.object.toString();
462
497
  if (result === null || result === undefined) {
463
498
  return `${result}`;
464
499
  }
@@ -498,9 +533,8 @@ class ASTUnary extends ASTNode {
498
533
  }
499
534
 
500
535
  codeFragment() {
501
- let result = `${ASTUnary.UnaryOpString[this.op]}${this.operand.codeFragment()}`;
502
-
503
- return result;
536
+ const operand = this.operand?.codeFragment ? this.operand.codeFragment() : '##ERROR##';
537
+ return `${ASTUnary.UnaryOpString[this.op]}${operand}`;
504
538
  }
505
539
 
506
540
  toString() {
@@ -568,11 +602,11 @@ class ASTBinary extends ASTNode {
568
602
  }
569
603
 
570
604
  get line() {
571
- return this.m_left.line;
605
+ return this.m_left ? this.m_left.line : this.m_lineNo;
572
606
  }
573
607
 
574
608
  get lastLine() {
575
- return this.m_right.line;
609
+ return this.m_right ? this.m_right.line : this.m_lineNo;
576
610
  }
577
611
 
578
612
  get isInplace() {
@@ -730,6 +764,13 @@ class ASTBinary extends ASTNode {
730
764
  case ASTBinary.BinOp.LeftShift:
731
765
  case ASTBinary.BinOp.RightShift:
732
766
  return 1;
767
+ // Python `and` binds tighter than `or`; ranking them so that
768
+ // AND wrapping OR forces parens keeps `(b or c) and d` from
769
+ // rendering as `b or c and d` (which parses as `b or (c and d)`).
770
+ case ASTBinary.BinOp.LogicalAnd:
771
+ return -1;
772
+ case ASTBinary.BinOp.LogicalOr:
773
+ return -2;
733
774
  default:
734
775
  return 0;
735
776
  }
@@ -744,6 +785,13 @@ class ASTBinary extends ASTNode {
744
785
  fragment = `(${fragment})`;
745
786
  }
746
787
  }
788
+ // `0.bit_length()` parses as a bad float literal; wrap integer
789
+ // literal targets of attribute access in parens.
790
+ if (this.op === ASTBinary.BinOp.Attr && position === 'left' &&
791
+ child instanceof ASTObject &&
792
+ child.object?.ClassName === 'Py_Int') {
793
+ fragment = `(${fragment})`;
794
+ }
747
795
  return fragment;
748
796
  };
749
797
 
@@ -944,15 +992,7 @@ class ASTStore extends ASTNode {
944
992
  let isAsync = (codeObject.Flags & ASTFunction.CodeFlags.CO_COROUTINE) ||
945
993
  (codeObject.Flags & ASTFunction.CodeFlags.CO_ASYNC_GENERATOR);
946
994
  result.add(isAsync ? "async ": "");
947
- const typeParams = (this.src.typeParams || []).map(tp => {
948
- if (tp?.codeFragment) {
949
- return tp.codeFragment();
950
- }
951
- if (tp?.toString) {
952
- return tp.toString();
953
- }
954
- return tp;
955
- });
995
+ const typeParams = (this.src.typeParams || []).map(tp => renderTypeParam(tp));
956
996
  const typeParamsStr = typeParams.length ? `[${typeParams.join(", ")}]` : "";
957
997
  result.lastLineAppend(`def ${destName}${typeParamsStr}(`);
958
998
  }
@@ -1004,29 +1044,58 @@ class ASTStore extends ASTNode {
1004
1044
  const rawKwName = toVarName(codeObject.VarNames.Value?.[argIndex++]);
1005
1045
  let argName = rawKwName;
1006
1046
 
1007
- // Check if this kwonly arg has a default value
1047
+ // Python syntax is `name: annotation = default`, so append the
1048
+ // annotation first and the default afterwards.
1049
+ const kwAnn = this.src.annotations?.[rawKwName];
1050
+ if (kwAnn) {
1051
+ argName = `${argName}: ${kwAnn.codeFragment?.() || kwAnn.toString?.() || '##ERROR##'}`;
1052
+ }
1008
1053
  if (default_params && default_params.length > 0) {
1009
1054
  let defaultIdx = kwIdx - (codeObject.KWOnlyArgCount - default_params.length);
1010
1055
  if (defaultIdx >= 0 && default_params[defaultIdx]) {
1011
1056
  argName += "=" + default_params[defaultIdx].value.codeFragment();
1012
1057
  }
1013
1058
  }
1014
- const kwAnn = this.src.annotations?.[rawKwName];
1015
- if (kwAnn) {
1016
- argName = `${argName}: ${kwAnn.codeFragment?.() || kwAnn.toString?.() || '##ERROR##'}`;
1017
- }
1018
1059
  argNames.push(argName);
1019
1060
  }
1020
1061
  }
1021
1062
  }
1022
1063
  if (codeObject.Flags & ASTFunction.CodeFlags.CO_VARARGS) {
1023
- argNames.push('*' + toVarName(codeObject.VarNames.Value?.[argIndex++]));
1064
+ const vaName = toVarName(codeObject.VarNames.Value?.[argIndex++]);
1065
+ let entry = '*' + vaName;
1066
+ const vaAnn = this.src.annotations?.[vaName];
1067
+ if (vaAnn) {
1068
+ entry += `: ${vaAnn.codeFragment?.() || vaAnn.toString?.() || '##ERROR##'}`;
1069
+ }
1070
+ if (codeObject.KWOnlyArgCount) {
1071
+ // Replace the placeholder "*" we already pushed with the named varargs.
1072
+ const starIdx = argNames.indexOf('*');
1073
+ if (starIdx >= 0) {
1074
+ argNames[starIdx] = entry;
1075
+ } else {
1076
+ argNames.push(entry);
1077
+ }
1078
+ } else {
1079
+ argNames.push(entry);
1080
+ }
1024
1081
  }
1025
1082
  if (codeObject.Flags & ASTFunction.CodeFlags.CO_VARKEYWORDS) {
1026
- argNames.push('**' + toVarName(codeObject.VarNames.Value?.[argIndex++]));
1083
+ const vkName = toVarName(codeObject.VarNames.Value?.[argIndex++]);
1084
+ let entry = '**' + vkName;
1085
+ const vkAnn = this.src.annotations?.[vkName];
1086
+ if (vkAnn) {
1087
+ entry += `: ${vkAnn.codeFragment?.() || vkAnn.toString?.() || '##ERROR##'}`;
1088
+ }
1089
+ argNames.push(entry);
1027
1090
  }
1028
1091
 
1029
- result.lastLineAppend((argNames.length > 0 ? " " : "") + argNames.join(", "));
1092
+ // Lambda needs a space after the `lambda` keyword before params (shouldTrim would
1093
+ // eat it); `def X(` already has the opening paren and wants no leading space.
1094
+ if (inLambda) {
1095
+ result.lastLineAppend((argNames.length > 0 ? " " : "") + argNames.join(", "), false);
1096
+ } else {
1097
+ result.lastLineAppend(argNames.join(", "));
1098
+ }
1030
1099
 
1031
1100
  if (inLambda) {
1032
1101
  result.lastLineAppend(": ", false);
@@ -1050,32 +1119,42 @@ class ASTStore extends ASTNode {
1050
1119
  if (inLambda) {
1051
1120
  result.lastLineAppend(methodBody);
1052
1121
  } else {
1053
- result.add(methodBody);
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
+ }
1054
1129
  }
1055
1130
  result.decreaseIndent();
1056
1131
 
1057
1132
  } else if (this.src instanceof ASTClass) {
1058
- if (this.decorators && this.decorators.length > 0) {
1059
- for (let decorator of this.decorators) {
1060
- const decoText = decorator?.codeFragment ? decorator.codeFragment() : decorator;
1061
- result.add('@' + decoText);
1062
- }
1133
+ const allDecorators = [
1134
+ ...(this.src.decorators || []),
1135
+ ...(this.decorators || []),
1136
+ ];
1137
+ for (let decorator of allDecorators) {
1138
+ const decoText = decorator?.codeFragment ? decorator.codeFragment() : decorator;
1139
+ result.add('@' + decoText);
1063
1140
  }
1064
1141
  let classNode = this.src;
1065
- const typeParams = (classNode.typeParams || []).map(tp => {
1066
- if (tp?.codeFragment) {
1067
- return tp.codeFragment();
1068
- }
1069
- if (tp?.toString) {
1070
- return tp.toString();
1071
- }
1072
- return tp;
1073
- });
1142
+ const typeParams = (classNode.typeParams || []).map(tp => renderTypeParam(tp));
1074
1143
  const typeParamsStr = typeParams.length ? `[${typeParams.join(", ")}]` : "";
1075
1144
  result.add(`class ${this.dest.codeFragment()}${typeParamsStr}`);
1076
1145
  const bases = classNode?.bases?.values || [];
1077
- if (bases.length > 0) {
1078
- result.lastLineAppend(`(${bases.map(node => node?.codeFragment() || "#ERROR##").join(", ")})`, false);
1146
+ const kwargs = classNode?.kwargs || [];
1147
+ const baseStrs = bases.map(node => node?.codeFragment() || "##ERROR##");
1148
+ const kwargStrs = kwargs.map(({key, value}) => {
1149
+ const keyStr = key?.object?.Value?.toString?.() ??
1150
+ key?.name ??
1151
+ (typeof key === 'string' ? key : key?.codeFragment?.()?.toString?.());
1152
+ const valStr = value?.codeFragment?.()?.toString?.() ?? "##ERROR##";
1153
+ return `${keyStr}=${valStr}`;
1154
+ });
1155
+ const headerArgs = [...baseStrs, ...kwargStrs];
1156
+ if (headerArgs.length > 0) {
1157
+ result.lastLineAppend(`(${headerArgs.join(", ")})`, false);
1079
1158
  }
1080
1159
  result.lastLineAppend(":");
1081
1160
  let codeObject = classNode.code?.func?.code?.object || classNode.code?.code?.object || {};
@@ -1097,16 +1176,9 @@ class ASTStore extends ASTNode {
1097
1176
  if (global.g_cliArgs?.debug) {
1098
1177
  console.log(`[ASTStore class render] name=${this.dest?.name}, len=${classBodyNodeList.length}, first=${classBodyNodeList[0]?.constructor?.name}, retVal=${classBodyNodeList[0]?.value?.constructor?.name}, isSyntheticEmptyReturn=${isSyntheticEmptyReturn}, hasContent=${hasContent}`);
1099
1178
  }
1100
- const hasMetaTypeBase = Array.isArray(classNode?.bases?.values) &&
1101
- classNode.bases.values.some(b => {
1102
- const frag = b?.codeFragment?.();
1103
- const str = frag?.toString?.();
1104
- return typeof str === 'string' && str.startsWith('type(');
1105
- });
1106
-
1107
1179
  if (hasContent && !isSyntheticEmptyReturn) {
1108
1180
  result.add(classBody);
1109
- } else if (!isSyntheticEmptyReturn && !hasMetaTypeBase) {
1181
+ } else if (!isSyntheticEmptyReturn) {
1110
1182
  result.add("pass");
1111
1183
  }
1112
1184
  result.decreaseIndent();
@@ -1271,6 +1343,23 @@ class ASTReturn extends ASTNode {
1271
1343
  return result;
1272
1344
  }
1273
1345
 
1346
+ switch (this.rettype) {
1347
+ case ASTReturn.RetType.Yield: {
1348
+ if (!this.value || this.value instanceof ASTNone) {
1349
+ return new PycResult("(yield)", true);
1350
+ }
1351
+ let yres = new PycResult("(yield ", true);
1352
+ yres.lastLineAppend(this.value.codeFragment());
1353
+ yres.lastLineAppend(")");
1354
+ return yres;
1355
+ }
1356
+ case ASTReturn.RetType.YieldFrom: {
1357
+ let yfres = new PycResult("(yield from ", true);
1358
+ yfres.lastLineAppend(this.value.codeFragment());
1359
+ yfres.lastLineAppend(")");
1360
+ return yfres;
1361
+ }
1362
+ }
1274
1363
  return this.value?.codeFragment() || "";
1275
1364
  }
1276
1365
 
@@ -1398,12 +1487,11 @@ class ASTFunction extends ASTNode {
1398
1487
  m_annotations = {};
1399
1488
  m_typeParams = [];
1400
1489
 
1401
- constructor(code, defargs = [], kwdefargs = [], annotations = []) {
1490
+ constructor(code, defargs = [], kwdefargs = []) {
1402
1491
  super();
1403
1492
  this.m_code = code;
1404
1493
  this.m_defargs = defargs;
1405
1494
  this.m_kwdefargs = kwdefargs;
1406
- this.m_decorators = annotations;
1407
1495
  }
1408
1496
 
1409
1497
  get code() {
@@ -1509,6 +1597,8 @@ class ASTClass extends ASTNode {
1509
1597
  m_bases = null;
1510
1598
  m_name = null;
1511
1599
  m_typeParams = [];
1600
+ m_kwargs = [];
1601
+ m_decorators = [];
1512
1602
 
1513
1603
  constructor(code, bases, name) {
1514
1604
  super();
@@ -1517,6 +1607,14 @@ class ASTClass extends ASTNode {
1517
1607
  this.m_name = name;
1518
1608
  }
1519
1609
 
1610
+ add_decorator(decorator) {
1611
+ this.m_decorators.unshift(decorator);
1612
+ }
1613
+
1614
+ get decorators() {
1615
+ return this.m_decorators;
1616
+ }
1617
+
1520
1618
  get code() {
1521
1619
  return this.m_code;
1522
1620
  }
@@ -1529,6 +1627,14 @@ class ASTClass extends ASTNode {
1529
1627
  return this.m_name;
1530
1628
  }
1531
1629
 
1630
+ get kwargs() {
1631
+ return this.m_kwargs || [];
1632
+ }
1633
+
1634
+ set kwargs(value) {
1635
+ this.m_kwargs = value || [];
1636
+ }
1637
+
1532
1638
  get typeParams() {
1533
1639
  return this.m_typeParams || [];
1534
1640
  }
@@ -1631,13 +1737,31 @@ class ASTCall extends ASTNode {
1631
1737
  params.push(this.pparams.map(formatParam).join(', ').trim());
1632
1738
  }
1633
1739
  if (this.kwparams?.length > 0) {
1634
- params.push(this.kwparams.map(node => `${node?.key?.codeFragment().toString().replaceAll("'",'')} = ${formatParam(node?.value)}`).join(', ').trim());
1740
+ params.push(this.kwparams.map(node => {
1741
+ // Keyword key is stored as a Py_String/Py_Unicode; extract raw identifier
1742
+ // rather than stripping quotes from the rendered literal (which misses
1743
+ // double quotes when the name contains a single quote, etc).
1744
+ let keyStr;
1745
+ const keyObj = node?.key;
1746
+ if (keyObj && keyObj.object && ['Py_String', 'Py_Unicode'].includes(keyObj.object.ClassName)) {
1747
+ keyStr = keyObj.object.toString();
1748
+ } else {
1749
+ keyStr = (keyObj?.codeFragment()?.toString() || '').replace(/^['"]|['"]$/g, '');
1750
+ }
1751
+ return `${keyStr}=${formatParam(node?.value)}`;
1752
+ }).join(', ').trim());
1635
1753
  }
1636
1754
  if (this.hasVar) {
1637
1755
  params.push('*' + this.var.codeFragment());
1638
1756
  }
1639
1757
  if (this.hasKw) {
1640
- params.push('**' + this.kw.codeFragment());
1758
+ if (this.kw instanceof ASTMapUnpack) {
1759
+ for (const item of this.kw.items) {
1760
+ params.push('**' + (item?.codeFragment?.() ?? '##ERROR##'));
1761
+ }
1762
+ } else {
1763
+ params.push('**' + this.kw.codeFragment());
1764
+ }
1641
1765
  }
1642
1766
  result.lastLineAppend(params.join(', ') + ')');
1643
1767
  return result;
@@ -1858,6 +1982,31 @@ class ASTMap extends ASTNode {
1858
1982
  }
1859
1983
  }
1860
1984
 
1985
+ // Py 3.5: BUILD_MAP_UNPACK / BUILD_MAP_UNPACK_WITH_CALL push this. Each item is
1986
+ // a mapping expression prefixed with ** at the source level. Items may be
1987
+ // literal ASTMap or arbitrary expressions (names, subscripts, calls).
1988
+ class ASTMapUnpack extends ASTNode {
1989
+ m_items = [];
1990
+
1991
+ constructor(items) {
1992
+ super();
1993
+ this.m_items = items || [];
1994
+ }
1995
+
1996
+ get items() {
1997
+ return this.m_items;
1998
+ }
1999
+
2000
+ codeFragment() {
2001
+ const parts = this.m_items.map(item => '**' + (item?.codeFragment?.() ?? '##ERROR##'));
2002
+ return '{' + parts.join(', ') + '}';
2003
+ }
2004
+
2005
+ toString() {
2006
+ return `ASTMapUnpack: line=${this.line}, ${this.codeFragment()}`;
2007
+ }
2008
+ }
2009
+
1861
2010
  class ASTKwNamesMap extends ASTNode {
1862
2011
  m_values = [];
1863
2012
 
@@ -2105,10 +2254,12 @@ class ASTKeyword extends ASTNode {
2105
2254
 
2106
2255
  class ASTRaise extends ASTNode {
2107
2256
  m_params = [];
2257
+ m_fromClause = false;
2108
2258
 
2109
- constructor(params) {
2259
+ constructor(params, fromClause = false) {
2110
2260
  super();
2111
2261
  this.m_params = params;
2262
+ this.m_fromClause = fromClause;
2112
2263
  }
2113
2264
 
2114
2265
  get params() {
@@ -2123,6 +2274,9 @@ class ASTRaise extends ASTNode {
2123
2274
  if (!this.params || this.params.length === 0) {
2124
2275
  return 'raise';
2125
2276
  }
2277
+ if (this.m_fromClause && this.params.length === 2) {
2278
+ return 'raise ' + this.params[0].codeFragment() + ' from ' + this.params[1].codeFragment();
2279
+ }
2126
2280
  return 'raise ' + this.params.map(node => node.codeFragment()).join(', ');
2127
2281
  }
2128
2282
 
@@ -2200,7 +2354,8 @@ class ASTBlock extends ASTNode {
2200
2354
  While: 8,
2201
2355
  For: 9,
2202
2356
  With: 10,
2203
- AsyncFor: 11
2357
+ AsyncFor: 11,
2358
+ AsyncWith: 12
2204
2359
  }
2205
2360
 
2206
2361
  m_blockType = ASTBlock.BlockType.Main;
@@ -2277,7 +2432,7 @@ class ASTBlock extends ASTNode {
2277
2432
  get type_str() {
2278
2433
  return [
2279
2434
  "", "if", "else", "elif", "try", "CONTAINER", "except",
2280
- "finally", "while", "for", "with", "async for"
2435
+ "finally", "while", "for", "with", "async for", "async with"
2281
2436
  ][this.blockType];
2282
2437
  }
2283
2438
 
@@ -2406,19 +2561,33 @@ class ASTBlock extends ASTNode {
2406
2561
  result.add("pass");
2407
2562
  }
2408
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 = [];
2409
2571
  renderNodes.map(node => {
2410
2572
  if (!node || node.skip) {
2411
2573
  return;
2412
2574
  }
2413
2575
  const isHandler = node.blockType == ASTBlock.BlockType.Except || node.blockType == ASTBlock.BlockType.Finally;
2414
2576
  if (isHandler) {
2415
- result.decreaseIndent();
2416
- result.add(node.codeFragment());
2417
- result.increaseIndent();
2577
+ handlerTail.push(node);
2418
2578
  } else {
2419
2579
  result.add(node.codeFragment());
2580
+ emittedBodyNode = true;
2420
2581
  }
2421
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
+ }
2422
2591
  }
2423
2592
  } else {
2424
2593
  result.add("pass");
@@ -2556,6 +2725,15 @@ class ASTCondBlock extends ASTBlock {
2556
2725
  node.name == '__exception__') {
2557
2726
  return false;
2558
2727
  }
2728
+ // Inside an except body, an empty else block is a control-flow
2729
+ // artifact (JUMP_ABSOLUTE leaving the handler with no body);
2730
+ // keeping it leaves the handler visually empty without `pass`.
2731
+ if (this.blockType == ASTBlock.BlockType.Except &&
2732
+ node instanceof ASTBlock &&
2733
+ node.blockType === ASTBlock.BlockType.Else &&
2734
+ node.empty()) {
2735
+ return false;
2736
+ }
2559
2737
  return node && !node.skip && node.codeFragment;
2560
2738
  });
2561
2739
 
@@ -2654,7 +2832,12 @@ class ASTIterBlock extends ASTBlock {
2654
2832
  result.lastLineAppend(this.iter?.codeFragment() || "##ERROR##");
2655
2833
  result.lastLineAppend(":");
2656
2834
  result.increaseIndent();
2657
- this.nodes.map(node => node && result.add(node.codeFragment()));
2835
+ const body = this.nodes.filter(Boolean);
2836
+ if (body.length === 0) {
2837
+ result.add("pass");
2838
+ } else {
2839
+ body.forEach(node => result.add(node.codeFragment()));
2840
+ }
2658
2841
  result.decreaseIndent();
2659
2842
 
2660
2843
  return result;
@@ -2753,9 +2936,70 @@ class ASTWithBlock extends ASTBlock {
2753
2936
 
2754
2937
  result.lastLineAppend(":");
2755
2938
  result.increaseIndent();
2756
- this.nodes.filter(Boolean).forEach(node => result.add(node.codeFragment()));
2939
+ const body = this.nodes.filter(Boolean);
2940
+ if (body.length === 0) {
2941
+ result.add("pass");
2942
+ } else {
2943
+ body.forEach(node => result.add(node.codeFragment()));
2944
+ }
2757
2945
  result.decreaseIndent();
2758
-
2946
+
2947
+ return result;
2948
+ }
2949
+
2950
+ toString() {
2951
+ return `${this.type_str} block: {${this.start} - ${this.end}}`;
2952
+ }
2953
+ }
2954
+
2955
+ class ASTAsyncWithBlock extends ASTBlock {
2956
+
2957
+ m_expr = null;
2958
+ m_var = null;
2959
+
2960
+ constructor(start = 0, end = 0) {
2961
+ super(ASTBlock.BlockType.AsyncWith, start, end);
2962
+ }
2963
+
2964
+ get expr() {
2965
+ return this.m_expr;
2966
+ }
2967
+
2968
+ set expr(value) {
2969
+ this.m_expr = value;
2970
+ }
2971
+
2972
+ get var() {
2973
+ return this.m_var;
2974
+ }
2975
+
2976
+ set var(value) {
2977
+ this.m_var = value;
2978
+ }
2979
+
2980
+ codeFragment() {
2981
+ let result = new PycResult();
2982
+ result.doNotIndent = true;
2983
+
2984
+ result.lastLineAppend("async with ", false);
2985
+ const exprCode = this.expr?.codeFragment ? this.expr.codeFragment() : "None";
2986
+ result.lastLineAppend(exprCode);
2987
+
2988
+ if (this.var) {
2989
+ result.lastLineAppend(" as ", false);
2990
+ result.lastLineAppend(this.var.codeFragment());
2991
+ }
2992
+
2993
+ result.lastLineAppend(":");
2994
+ result.increaseIndent();
2995
+ const body = this.nodes.filter(Boolean);
2996
+ if (body.length === 0) {
2997
+ result.add("pass");
2998
+ } else {
2999
+ body.forEach(node => result.add(node.codeFragment()));
3000
+ }
3001
+ result.decreaseIndent();
3002
+
2759
3003
  return result;
2760
3004
  }
2761
3005
 
@@ -2769,6 +3013,7 @@ class ASTComprehension extends ASTNode {
2769
3013
  static LIST = 0;
2770
3014
  static SET = 1;
2771
3015
  static DICT = 2;
3016
+ static GENERATOR = 3;
2772
3017
 
2773
3018
  m_kind = ASTComprehension.LIST;
2774
3019
  m_key = null;
@@ -2820,6 +3065,9 @@ class ASTComprehension extends ASTNode {
2820
3065
  if ([ASTComprehension.SET, ASTComprehension.DICT].includes(this.kind)) {
2821
3066
  openingBracket = '{';
2822
3067
  closingBracket = '}';
3068
+ } else if (this.kind == ASTComprehension.GENERATOR) {
3069
+ openingBracket = '(';
3070
+ closingBracket = ')';
2823
3071
  }
2824
3072
 
2825
3073
  if (!this.result) {
@@ -2829,7 +3077,8 @@ class ASTComprehension extends ASTNode {
2829
3077
  let result = `${openingBracket}${this.kind == ASTComprehension.DICT ? (this.key?.codeFragment() || "##ERROR##") + ": " : ""}${this.result.codeFragment?.() || "##ERROR##"}`;
2830
3078
 
2831
3079
  result += this.generators.map(gen => {
2832
- let genString = ` for ${gen.index?.codeFragment?.() || "##ERROR##"} in ${gen.iter?.codeFragment?.() || "##ERROR##"}`;
3080
+ let asyncPrefix = gen.blockType == ASTBlock.BlockType.AsyncFor ? 'async ' : '';
3081
+ let genString = ` ${asyncPrefix}for ${gen.index?.codeFragment?.() || "##ERROR##"} in ${gen.iter?.codeFragment?.() || "##ERROR##"}`;
2833
3082
 
2834
3083
  if (gen.condition) {
2835
3084
  genString += ` if ${gen.condition.codeFragment?.() || "##ERROR##"}`;
@@ -2924,12 +3173,27 @@ class ASTFormattedValue extends ASTNode {
2924
3173
  return this.m_format_spec;
2925
3174
  }
2926
3175
 
2927
- codeFragment() {
3176
+ codeFragment(outerQuote = null) {
2928
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 === "'" ? '"' : '"';
2929
3183
  let result = "{";
2930
3184
 
2931
3185
  if (this.m_val) {
2932
- result += this.m_val.codeFragment();
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
+ }
2933
3197
  }
2934
3198
 
2935
3199
  // Add conversion flag (!s, !r, !a)
@@ -2950,7 +3214,10 @@ class ASTFormattedValue extends ASTNode {
2950
3214
  result += ":";
2951
3215
  // Format spec can be ASTJoinedStr (nested f-string) or string constant
2952
3216
  if (this.m_format_spec instanceof ASTJoinedStr) {
2953
- result += this.m_format_spec.codeFragment();
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);
2954
3221
  } else if (this.m_format_spec instanceof ASTObject) {
2955
3222
  // String constant like ".2f"
2956
3223
  result += this.m_format_spec.object.Value;
@@ -2983,9 +3250,14 @@ class ASTJoinedStr extends ASTNode {
2983
3250
  get lastLine() {
2984
3251
  return this.values[this.values.length - 1]?.lastLine;
2985
3252
  }
2986
- codeFragment() {
2987
- // f-string format: f"literal {expr} literal"
2988
- let result = 'f"';
3253
+ codeFragment(quoteChar = '"', bareInnerForFormatSpec = false) {
3254
+ // PEP 750 t-strings use t"..." prefix; otherwise f-string.
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}`;
2989
3261
 
2990
3262
  // Values are in reverse order (BUILD_STRING pops from stack)
2991
3263
  // So we need to reverse them
@@ -2994,6 +3266,12 @@ class ASTJoinedStr extends ASTNode {
2994
3266
  for (let i = 0; i < values.length; i++) {
2995
3267
  let value = values[i];
2996
3268
 
3269
+ // Stack underflow during BUILD_STRING leaves undefined slots; skip them
3270
+ // rather than crashing post-decompile passes that re-render this node.
3271
+ if (value == null) {
3272
+ continue;
3273
+ }
3274
+
2997
3275
  if (value instanceof ASTFormattedValue) {
2998
3276
  // Check for f-string = debugging pattern (Python 3.8+)
2999
3277
  // Pattern: literal ending with "varname=" followed by {varname!r}
@@ -3013,8 +3291,10 @@ class ASTJoinedStr extends ASTNode {
3013
3291
  let prefix = prevStr.substring(0, prevStr.length - match[0].length);
3014
3292
 
3015
3293
  // Remove the previously added literal and replace with prefix + {var=}
3294
+ let quoteEsc = quoteChar === '"' ? /"/g : /'/g;
3295
+ let quoteReplace = quoteChar === '"' ? '\\"' : "\\'";
3016
3296
  let beforeLiteral = result.lastIndexOf(prevStr.replace(/\\/g, '\\\\')
3017
- .replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\t/g, '\\t'));
3297
+ .replace(quoteEsc, quoteReplace).replace(/\n/g, '\\n').replace(/\t/g, '\\t'));
3018
3298
 
3019
3299
  if (beforeLiteral !== -1) {
3020
3300
  result = result.substring(0, beforeLiteral);
@@ -3023,7 +3303,7 @@ class ASTJoinedStr extends ASTNode {
3023
3303
  // Add prefix (escaped) if present
3024
3304
  if (prefix) {
3025
3305
  let escapedPrefix = prefix.replace(/\\/g, '\\\\');
3026
- escapedPrefix = escapedPrefix.replace(/"/g, '\\"');
3306
+ escapedPrefix = escapedPrefix.replace(quoteEsc, quoteReplace);
3027
3307
  escapedPrefix = escapedPrefix.replace(/\n/g, '\\n');
3028
3308
  escapedPrefix = escapedPrefix.replace(/\t/g, '\\t');
3029
3309
  result += escapedPrefix;
@@ -3035,16 +3315,25 @@ class ASTJoinedStr extends ASTNode {
3035
3315
  }
3036
3316
  }
3037
3317
 
3038
- // {expression} part
3039
- result += value.codeFragment();
3040
- } else if (value instanceof ASTObject && value.object?.ClassName === 'Py_String') {
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)) {
3041
3323
  // Literal string part - need to escape special chars
3042
3324
  let str = value.object.Value;
3043
- // Escape backslashes and quotes
3325
+ // Escape backslashes and the current quote char
3044
3326
  str = str.replace(/\\/g, '\\\\');
3045
- str = str.replace(/"/g, '\\"');
3327
+ if (quoteChar === '"') {
3328
+ str = str.replace(/"/g, '\\"');
3329
+ } else {
3330
+ str = str.replace(/'/g, "\\'");
3331
+ }
3046
3332
  str = str.replace(/\n/g, '\\n');
3333
+ str = str.replace(/\r/g, '\\r');
3047
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'));
3048
3337
  result += str;
3049
3338
  } else {
3050
3339
  // Fallback for unexpected types
@@ -3052,7 +3341,9 @@ class ASTJoinedStr extends ASTNode {
3052
3341
  }
3053
3342
  }
3054
3343
 
3055
- result += '"';
3344
+ if (!bareInnerForFormatSpec) {
3345
+ result += quoteChar;
3346
+ }
3056
3347
  return result;
3057
3348
  }
3058
3349
 
@@ -3379,7 +3670,7 @@ class ASTPattern extends ASTNode {
3379
3670
  return '##ERROR##';
3380
3671
 
3381
3672
  default:
3382
- return '#TODO pattern';
3673
+ throw new Error(`ASTPattern.codeFragment(): unsupported pattern type '${this.m_type}'`);
3383
3674
  }
3384
3675
  }
3385
3676
 
@@ -3412,6 +3703,7 @@ module.exports = {
3412
3703
  ASTList,
3413
3704
  ASTSet,
3414
3705
  ASTMap,
3706
+ ASTMapUnpack,
3415
3707
  ASTKwNamesMap,
3416
3708
  ASTConstMap,
3417
3709
  ASTSubscr,
@@ -3425,6 +3717,7 @@ module.exports = {
3425
3717
  ASTIterBlock,
3426
3718
  ASTContainerBlock,
3427
3719
  ASTWithBlock,
3720
+ ASTAsyncWithBlock,
3428
3721
  ASTComprehension,
3429
3722
  ASTLoadBuildClass,
3430
3723
  ASTAwaitable,