depyo 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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) {
@@ -446,19 +455,29 @@ class ASTObject extends ASTNode {
446
455
  }
447
456
  }
448
457
 
449
- let result = this.object.toString();
450
458
  if (["Py_String", "Py_Unicode"].includes(this.object.ClassName)) {
459
+ let raw = this.object.Value;
460
+ if (raw == null || raw.length === 0) {
461
+ return '""';
462
+ }
463
+ raw = raw.toString();
464
+ let escaped = raw
465
+ .replace(/\\/g, '\\\\')
466
+ .replace(/\n/g, '\\n')
467
+ .replace(/\r/g, '\\r')
468
+ .replace(/\t/g, '\\t');
451
469
  let quote = '"';
452
- if (result.includes('"')) {
453
- if (!result.includes("'")) {
470
+ if (escaped.includes('"')) {
471
+ if (!escaped.includes("'")) {
454
472
  quote = "'";
455
473
  } else {
456
- quote = '"';
457
- result = result.replace(/"/g, '\\"');
474
+ escaped = escaped.replace(/"/g, '\\"');
458
475
  }
459
476
  }
460
- return quote + result + quote;
477
+ return quote + escaped + quote;
461
478
  }
479
+
480
+ let result = this.object.toString();
462
481
  if (result === null || result === undefined) {
463
482
  return `${result}`;
464
483
  }
@@ -498,9 +517,8 @@ class ASTUnary extends ASTNode {
498
517
  }
499
518
 
500
519
  codeFragment() {
501
- let result = `${ASTUnary.UnaryOpString[this.op]}${this.operand.codeFragment()}`;
502
-
503
- return result;
520
+ const operand = this.operand?.codeFragment ? this.operand.codeFragment() : '##ERROR##';
521
+ return `${ASTUnary.UnaryOpString[this.op]}${operand}`;
504
522
  }
505
523
 
506
524
  toString() {
@@ -568,11 +586,11 @@ class ASTBinary extends ASTNode {
568
586
  }
569
587
 
570
588
  get line() {
571
- return this.m_left.line;
589
+ return this.m_left ? this.m_left.line : this.m_lineNo;
572
590
  }
573
591
 
574
592
  get lastLine() {
575
- return this.m_right.line;
593
+ return this.m_right ? this.m_right.line : this.m_lineNo;
576
594
  }
577
595
 
578
596
  get isInplace() {
@@ -730,6 +748,13 @@ class ASTBinary extends ASTNode {
730
748
  case ASTBinary.BinOp.LeftShift:
731
749
  case ASTBinary.BinOp.RightShift:
732
750
  return 1;
751
+ // Python `and` binds tighter than `or`; ranking them so that
752
+ // AND wrapping OR forces parens keeps `(b or c) and d` from
753
+ // rendering as `b or c and d` (which parses as `b or (c and d)`).
754
+ case ASTBinary.BinOp.LogicalAnd:
755
+ return -1;
756
+ case ASTBinary.BinOp.LogicalOr:
757
+ return -2;
733
758
  default:
734
759
  return 0;
735
760
  }
@@ -744,6 +769,13 @@ class ASTBinary extends ASTNode {
744
769
  fragment = `(${fragment})`;
745
770
  }
746
771
  }
772
+ // `0.bit_length()` parses as a bad float literal; wrap integer
773
+ // literal targets of attribute access in parens.
774
+ if (this.op === ASTBinary.BinOp.Attr && position === 'left' &&
775
+ child instanceof ASTObject &&
776
+ child.object?.ClassName === 'Py_Int') {
777
+ fragment = `(${fragment})`;
778
+ }
747
779
  return fragment;
748
780
  };
749
781
 
@@ -944,15 +976,7 @@ class ASTStore extends ASTNode {
944
976
  let isAsync = (codeObject.Flags & ASTFunction.CodeFlags.CO_COROUTINE) ||
945
977
  (codeObject.Flags & ASTFunction.CodeFlags.CO_ASYNC_GENERATOR);
946
978
  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
- });
979
+ const typeParams = (this.src.typeParams || []).map(tp => renderTypeParam(tp));
956
980
  const typeParamsStr = typeParams.length ? `[${typeParams.join(", ")}]` : "";
957
981
  result.lastLineAppend(`def ${destName}${typeParamsStr}(`);
958
982
  }
@@ -1004,29 +1028,58 @@ class ASTStore extends ASTNode {
1004
1028
  const rawKwName = toVarName(codeObject.VarNames.Value?.[argIndex++]);
1005
1029
  let argName = rawKwName;
1006
1030
 
1007
- // Check if this kwonly arg has a default value
1031
+ // Python syntax is `name: annotation = default`, so append the
1032
+ // annotation first and the default afterwards.
1033
+ const kwAnn = this.src.annotations?.[rawKwName];
1034
+ if (kwAnn) {
1035
+ argName = `${argName}: ${kwAnn.codeFragment?.() || kwAnn.toString?.() || '##ERROR##'}`;
1036
+ }
1008
1037
  if (default_params && default_params.length > 0) {
1009
1038
  let defaultIdx = kwIdx - (codeObject.KWOnlyArgCount - default_params.length);
1010
1039
  if (defaultIdx >= 0 && default_params[defaultIdx]) {
1011
1040
  argName += "=" + default_params[defaultIdx].value.codeFragment();
1012
1041
  }
1013
1042
  }
1014
- const kwAnn = this.src.annotations?.[rawKwName];
1015
- if (kwAnn) {
1016
- argName = `${argName}: ${kwAnn.codeFragment?.() || kwAnn.toString?.() || '##ERROR##'}`;
1017
- }
1018
1043
  argNames.push(argName);
1019
1044
  }
1020
1045
  }
1021
1046
  }
1022
1047
  if (codeObject.Flags & ASTFunction.CodeFlags.CO_VARARGS) {
1023
- argNames.push('*' + toVarName(codeObject.VarNames.Value?.[argIndex++]));
1048
+ const vaName = toVarName(codeObject.VarNames.Value?.[argIndex++]);
1049
+ let entry = '*' + vaName;
1050
+ const vaAnn = this.src.annotations?.[vaName];
1051
+ if (vaAnn) {
1052
+ entry += `: ${vaAnn.codeFragment?.() || vaAnn.toString?.() || '##ERROR##'}`;
1053
+ }
1054
+ if (codeObject.KWOnlyArgCount) {
1055
+ // Replace the placeholder "*" we already pushed with the named varargs.
1056
+ const starIdx = argNames.indexOf('*');
1057
+ if (starIdx >= 0) {
1058
+ argNames[starIdx] = entry;
1059
+ } else {
1060
+ argNames.push(entry);
1061
+ }
1062
+ } else {
1063
+ argNames.push(entry);
1064
+ }
1024
1065
  }
1025
1066
  if (codeObject.Flags & ASTFunction.CodeFlags.CO_VARKEYWORDS) {
1026
- argNames.push('**' + toVarName(codeObject.VarNames.Value?.[argIndex++]));
1067
+ const vkName = toVarName(codeObject.VarNames.Value?.[argIndex++]);
1068
+ let entry = '**' + vkName;
1069
+ const vkAnn = this.src.annotations?.[vkName];
1070
+ if (vkAnn) {
1071
+ entry += `: ${vkAnn.codeFragment?.() || vkAnn.toString?.() || '##ERROR##'}`;
1072
+ }
1073
+ argNames.push(entry);
1027
1074
  }
1028
1075
 
1029
- result.lastLineAppend((argNames.length > 0 ? " " : "") + argNames.join(", "));
1076
+ // Lambda needs a space after the `lambda` keyword before params (shouldTrim would
1077
+ // eat it); `def X(` already has the opening paren and wants no leading space.
1078
+ if (inLambda) {
1079
+ result.lastLineAppend((argNames.length > 0 ? " " : "") + argNames.join(", "), false);
1080
+ } else {
1081
+ result.lastLineAppend(argNames.join(", "));
1082
+ }
1030
1083
 
1031
1084
  if (inLambda) {
1032
1085
  result.lastLineAppend(": ", false);
@@ -1055,27 +1108,31 @@ class ASTStore extends ASTNode {
1055
1108
  result.decreaseIndent();
1056
1109
 
1057
1110
  } 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
- }
1111
+ const allDecorators = [
1112
+ ...(this.src.decorators || []),
1113
+ ...(this.decorators || []),
1114
+ ];
1115
+ for (let decorator of allDecorators) {
1116
+ const decoText = decorator?.codeFragment ? decorator.codeFragment() : decorator;
1117
+ result.add('@' + decoText);
1063
1118
  }
1064
1119
  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
- });
1120
+ const typeParams = (classNode.typeParams || []).map(tp => renderTypeParam(tp));
1074
1121
  const typeParamsStr = typeParams.length ? `[${typeParams.join(", ")}]` : "";
1075
1122
  result.add(`class ${this.dest.codeFragment()}${typeParamsStr}`);
1076
1123
  const bases = classNode?.bases?.values || [];
1077
- if (bases.length > 0) {
1078
- result.lastLineAppend(`(${bases.map(node => node?.codeFragment() || "#ERROR##").join(", ")})`, false);
1124
+ const kwargs = classNode?.kwargs || [];
1125
+ const baseStrs = bases.map(node => node?.codeFragment() || "##ERROR##");
1126
+ const kwargStrs = kwargs.map(({key, value}) => {
1127
+ const keyStr = key?.object?.Value?.toString?.() ??
1128
+ key?.name ??
1129
+ (typeof key === 'string' ? key : key?.codeFragment?.()?.toString?.());
1130
+ const valStr = value?.codeFragment?.()?.toString?.() ?? "##ERROR##";
1131
+ return `${keyStr}=${valStr}`;
1132
+ });
1133
+ const headerArgs = [...baseStrs, ...kwargStrs];
1134
+ if (headerArgs.length > 0) {
1135
+ result.lastLineAppend(`(${headerArgs.join(", ")})`, false);
1079
1136
  }
1080
1137
  result.lastLineAppend(":");
1081
1138
  let codeObject = classNode.code?.func?.code?.object || classNode.code?.code?.object || {};
@@ -1097,16 +1154,9 @@ class ASTStore extends ASTNode {
1097
1154
  if (global.g_cliArgs?.debug) {
1098
1155
  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
1156
  }
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
1157
  if (hasContent && !isSyntheticEmptyReturn) {
1108
1158
  result.add(classBody);
1109
- } else if (!isSyntheticEmptyReturn && !hasMetaTypeBase) {
1159
+ } else if (!isSyntheticEmptyReturn) {
1110
1160
  result.add("pass");
1111
1161
  }
1112
1162
  result.decreaseIndent();
@@ -1271,6 +1321,23 @@ class ASTReturn extends ASTNode {
1271
1321
  return result;
1272
1322
  }
1273
1323
 
1324
+ switch (this.rettype) {
1325
+ case ASTReturn.RetType.Yield: {
1326
+ if (!this.value || this.value instanceof ASTNone) {
1327
+ return new PycResult("(yield)", true);
1328
+ }
1329
+ let yres = new PycResult("(yield ", true);
1330
+ yres.lastLineAppend(this.value.codeFragment());
1331
+ yres.lastLineAppend(")");
1332
+ return yres;
1333
+ }
1334
+ case ASTReturn.RetType.YieldFrom: {
1335
+ let yfres = new PycResult("(yield from ", true);
1336
+ yfres.lastLineAppend(this.value.codeFragment());
1337
+ yfres.lastLineAppend(")");
1338
+ return yfres;
1339
+ }
1340
+ }
1274
1341
  return this.value?.codeFragment() || "";
1275
1342
  }
1276
1343
 
@@ -1398,12 +1465,11 @@ class ASTFunction extends ASTNode {
1398
1465
  m_annotations = {};
1399
1466
  m_typeParams = [];
1400
1467
 
1401
- constructor(code, defargs = [], kwdefargs = [], annotations = []) {
1468
+ constructor(code, defargs = [], kwdefargs = []) {
1402
1469
  super();
1403
1470
  this.m_code = code;
1404
1471
  this.m_defargs = defargs;
1405
1472
  this.m_kwdefargs = kwdefargs;
1406
- this.m_decorators = annotations;
1407
1473
  }
1408
1474
 
1409
1475
  get code() {
@@ -1509,6 +1575,8 @@ class ASTClass extends ASTNode {
1509
1575
  m_bases = null;
1510
1576
  m_name = null;
1511
1577
  m_typeParams = [];
1578
+ m_kwargs = [];
1579
+ m_decorators = [];
1512
1580
 
1513
1581
  constructor(code, bases, name) {
1514
1582
  super();
@@ -1517,6 +1585,14 @@ class ASTClass extends ASTNode {
1517
1585
  this.m_name = name;
1518
1586
  }
1519
1587
 
1588
+ add_decorator(decorator) {
1589
+ this.m_decorators.unshift(decorator);
1590
+ }
1591
+
1592
+ get decorators() {
1593
+ return this.m_decorators;
1594
+ }
1595
+
1520
1596
  get code() {
1521
1597
  return this.m_code;
1522
1598
  }
@@ -1529,6 +1605,14 @@ class ASTClass extends ASTNode {
1529
1605
  return this.m_name;
1530
1606
  }
1531
1607
 
1608
+ get kwargs() {
1609
+ return this.m_kwargs || [];
1610
+ }
1611
+
1612
+ set kwargs(value) {
1613
+ this.m_kwargs = value || [];
1614
+ }
1615
+
1532
1616
  get typeParams() {
1533
1617
  return this.m_typeParams || [];
1534
1618
  }
@@ -1631,13 +1715,31 @@ class ASTCall extends ASTNode {
1631
1715
  params.push(this.pparams.map(formatParam).join(', ').trim());
1632
1716
  }
1633
1717
  if (this.kwparams?.length > 0) {
1634
- params.push(this.kwparams.map(node => `${node?.key?.codeFragment().toString().replaceAll("'",'')} = ${formatParam(node?.value)}`).join(', ').trim());
1718
+ params.push(this.kwparams.map(node => {
1719
+ // Keyword key is stored as a Py_String/Py_Unicode; extract raw identifier
1720
+ // rather than stripping quotes from the rendered literal (which misses
1721
+ // double quotes when the name contains a single quote, etc).
1722
+ let keyStr;
1723
+ const keyObj = node?.key;
1724
+ if (keyObj && keyObj.object && ['Py_String', 'Py_Unicode'].includes(keyObj.object.ClassName)) {
1725
+ keyStr = keyObj.object.toString();
1726
+ } else {
1727
+ keyStr = (keyObj?.codeFragment()?.toString() || '').replace(/^['"]|['"]$/g, '');
1728
+ }
1729
+ return `${keyStr}=${formatParam(node?.value)}`;
1730
+ }).join(', ').trim());
1635
1731
  }
1636
1732
  if (this.hasVar) {
1637
1733
  params.push('*' + this.var.codeFragment());
1638
1734
  }
1639
1735
  if (this.hasKw) {
1640
- params.push('**' + this.kw.codeFragment());
1736
+ if (this.kw instanceof ASTMapUnpack) {
1737
+ for (const item of this.kw.items) {
1738
+ params.push('**' + (item?.codeFragment?.() ?? '##ERROR##'));
1739
+ }
1740
+ } else {
1741
+ params.push('**' + this.kw.codeFragment());
1742
+ }
1641
1743
  }
1642
1744
  result.lastLineAppend(params.join(', ') + ')');
1643
1745
  return result;
@@ -1858,6 +1960,31 @@ class ASTMap extends ASTNode {
1858
1960
  }
1859
1961
  }
1860
1962
 
1963
+ // Py 3.5: BUILD_MAP_UNPACK / BUILD_MAP_UNPACK_WITH_CALL push this. Each item is
1964
+ // a mapping expression prefixed with ** at the source level. Items may be
1965
+ // literal ASTMap or arbitrary expressions (names, subscripts, calls).
1966
+ class ASTMapUnpack extends ASTNode {
1967
+ m_items = [];
1968
+
1969
+ constructor(items) {
1970
+ super();
1971
+ this.m_items = items || [];
1972
+ }
1973
+
1974
+ get items() {
1975
+ return this.m_items;
1976
+ }
1977
+
1978
+ codeFragment() {
1979
+ const parts = this.m_items.map(item => '**' + (item?.codeFragment?.() ?? '##ERROR##'));
1980
+ return '{' + parts.join(', ') + '}';
1981
+ }
1982
+
1983
+ toString() {
1984
+ return `ASTMapUnpack: line=${this.line}, ${this.codeFragment()}`;
1985
+ }
1986
+ }
1987
+
1861
1988
  class ASTKwNamesMap extends ASTNode {
1862
1989
  m_values = [];
1863
1990
 
@@ -2105,10 +2232,12 @@ class ASTKeyword extends ASTNode {
2105
2232
 
2106
2233
  class ASTRaise extends ASTNode {
2107
2234
  m_params = [];
2235
+ m_fromClause = false;
2108
2236
 
2109
- constructor(params) {
2237
+ constructor(params, fromClause = false) {
2110
2238
  super();
2111
2239
  this.m_params = params;
2240
+ this.m_fromClause = fromClause;
2112
2241
  }
2113
2242
 
2114
2243
  get params() {
@@ -2123,6 +2252,9 @@ class ASTRaise extends ASTNode {
2123
2252
  if (!this.params || this.params.length === 0) {
2124
2253
  return 'raise';
2125
2254
  }
2255
+ if (this.m_fromClause && this.params.length === 2) {
2256
+ return 'raise ' + this.params[0].codeFragment() + ' from ' + this.params[1].codeFragment();
2257
+ }
2126
2258
  return 'raise ' + this.params.map(node => node.codeFragment()).join(', ');
2127
2259
  }
2128
2260
 
@@ -2200,7 +2332,8 @@ class ASTBlock extends ASTNode {
2200
2332
  While: 8,
2201
2333
  For: 9,
2202
2334
  With: 10,
2203
- AsyncFor: 11
2335
+ AsyncFor: 11,
2336
+ AsyncWith: 12
2204
2337
  }
2205
2338
 
2206
2339
  m_blockType = ASTBlock.BlockType.Main;
@@ -2277,7 +2410,7 @@ class ASTBlock extends ASTNode {
2277
2410
  get type_str() {
2278
2411
  return [
2279
2412
  "", "if", "else", "elif", "try", "CONTAINER", "except",
2280
- "finally", "while", "for", "with", "async for"
2413
+ "finally", "while", "for", "with", "async for", "async with"
2281
2414
  ][this.blockType];
2282
2415
  }
2283
2416
 
@@ -2556,6 +2689,15 @@ class ASTCondBlock extends ASTBlock {
2556
2689
  node.name == '__exception__') {
2557
2690
  return false;
2558
2691
  }
2692
+ // Inside an except body, an empty else block is a control-flow
2693
+ // artifact (JUMP_ABSOLUTE leaving the handler with no body);
2694
+ // keeping it leaves the handler visually empty without `pass`.
2695
+ if (this.blockType == ASTBlock.BlockType.Except &&
2696
+ node instanceof ASTBlock &&
2697
+ node.blockType === ASTBlock.BlockType.Else &&
2698
+ node.empty()) {
2699
+ return false;
2700
+ }
2559
2701
  return node && !node.skip && node.codeFragment;
2560
2702
  });
2561
2703
 
@@ -2654,7 +2796,12 @@ class ASTIterBlock extends ASTBlock {
2654
2796
  result.lastLineAppend(this.iter?.codeFragment() || "##ERROR##");
2655
2797
  result.lastLineAppend(":");
2656
2798
  result.increaseIndent();
2657
- this.nodes.map(node => node && result.add(node.codeFragment()));
2799
+ const body = this.nodes.filter(Boolean);
2800
+ if (body.length === 0) {
2801
+ result.add("pass");
2802
+ } else {
2803
+ body.forEach(node => result.add(node.codeFragment()));
2804
+ }
2658
2805
  result.decreaseIndent();
2659
2806
 
2660
2807
  return result;
@@ -2753,9 +2900,70 @@ class ASTWithBlock extends ASTBlock {
2753
2900
 
2754
2901
  result.lastLineAppend(":");
2755
2902
  result.increaseIndent();
2756
- this.nodes.filter(Boolean).forEach(node => result.add(node.codeFragment()));
2903
+ const body = this.nodes.filter(Boolean);
2904
+ if (body.length === 0) {
2905
+ result.add("pass");
2906
+ } else {
2907
+ body.forEach(node => result.add(node.codeFragment()));
2908
+ }
2757
2909
  result.decreaseIndent();
2758
-
2910
+
2911
+ return result;
2912
+ }
2913
+
2914
+ toString() {
2915
+ return `${this.type_str} block: {${this.start} - ${this.end}}`;
2916
+ }
2917
+ }
2918
+
2919
+ class ASTAsyncWithBlock extends ASTBlock {
2920
+
2921
+ m_expr = null;
2922
+ m_var = null;
2923
+
2924
+ constructor(start = 0, end = 0) {
2925
+ super(ASTBlock.BlockType.AsyncWith, start, end);
2926
+ }
2927
+
2928
+ get expr() {
2929
+ return this.m_expr;
2930
+ }
2931
+
2932
+ set expr(value) {
2933
+ this.m_expr = value;
2934
+ }
2935
+
2936
+ get var() {
2937
+ return this.m_var;
2938
+ }
2939
+
2940
+ set var(value) {
2941
+ this.m_var = value;
2942
+ }
2943
+
2944
+ codeFragment() {
2945
+ let result = new PycResult();
2946
+ result.doNotIndent = true;
2947
+
2948
+ result.lastLineAppend("async with ", false);
2949
+ const exprCode = this.expr?.codeFragment ? this.expr.codeFragment() : "None";
2950
+ result.lastLineAppend(exprCode);
2951
+
2952
+ if (this.var) {
2953
+ result.lastLineAppend(" as ", false);
2954
+ result.lastLineAppend(this.var.codeFragment());
2955
+ }
2956
+
2957
+ result.lastLineAppend(":");
2958
+ result.increaseIndent();
2959
+ const body = this.nodes.filter(Boolean);
2960
+ if (body.length === 0) {
2961
+ result.add("pass");
2962
+ } else {
2963
+ body.forEach(node => result.add(node.codeFragment()));
2964
+ }
2965
+ result.decreaseIndent();
2966
+
2759
2967
  return result;
2760
2968
  }
2761
2969
 
@@ -2769,6 +2977,7 @@ class ASTComprehension extends ASTNode {
2769
2977
  static LIST = 0;
2770
2978
  static SET = 1;
2771
2979
  static DICT = 2;
2980
+ static GENERATOR = 3;
2772
2981
 
2773
2982
  m_kind = ASTComprehension.LIST;
2774
2983
  m_key = null;
@@ -2820,6 +3029,9 @@ class ASTComprehension extends ASTNode {
2820
3029
  if ([ASTComprehension.SET, ASTComprehension.DICT].includes(this.kind)) {
2821
3030
  openingBracket = '{';
2822
3031
  closingBracket = '}';
3032
+ } else if (this.kind == ASTComprehension.GENERATOR) {
3033
+ openingBracket = '(';
3034
+ closingBracket = ')';
2823
3035
  }
2824
3036
 
2825
3037
  if (!this.result) {
@@ -2829,7 +3041,8 @@ class ASTComprehension extends ASTNode {
2829
3041
  let result = `${openingBracket}${this.kind == ASTComprehension.DICT ? (this.key?.codeFragment() || "##ERROR##") + ": " : ""}${this.result.codeFragment?.() || "##ERROR##"}`;
2830
3042
 
2831
3043
  result += this.generators.map(gen => {
2832
- let genString = ` for ${gen.index?.codeFragment?.() || "##ERROR##"} in ${gen.iter?.codeFragment?.() || "##ERROR##"}`;
3044
+ let asyncPrefix = gen.blockType == ASTBlock.BlockType.AsyncFor ? 'async ' : '';
3045
+ let genString = ` ${asyncPrefix}for ${gen.index?.codeFragment?.() || "##ERROR##"} in ${gen.iter?.codeFragment?.() || "##ERROR##"}`;
2833
3046
 
2834
3047
  if (gen.condition) {
2835
3048
  genString += ` if ${gen.condition.codeFragment?.() || "##ERROR##"}`;
@@ -2984,8 +3197,8 @@ class ASTJoinedStr extends ASTNode {
2984
3197
  return this.values[this.values.length - 1]?.lastLine;
2985
3198
  }
2986
3199
  codeFragment() {
2987
- // f-string format: f"literal {expr} literal"
2988
- let result = 'f"';
3200
+ // PEP 750 t-strings use t"..." prefix; otherwise f-string.
3201
+ let result = (this.isTemplateString ? 't"' : 'f"');
2989
3202
 
2990
3203
  // Values are in reverse order (BUILD_STRING pops from stack)
2991
3204
  // So we need to reverse them
@@ -2994,6 +3207,12 @@ class ASTJoinedStr extends ASTNode {
2994
3207
  for (let i = 0; i < values.length; i++) {
2995
3208
  let value = values[i];
2996
3209
 
3210
+ // Stack underflow during BUILD_STRING leaves undefined slots; skip them
3211
+ // rather than crashing post-decompile passes that re-render this node.
3212
+ if (value == null) {
3213
+ continue;
3214
+ }
3215
+
2997
3216
  if (value instanceof ASTFormattedValue) {
2998
3217
  // Check for f-string = debugging pattern (Python 3.8+)
2999
3218
  // Pattern: literal ending with "varname=" followed by {varname!r}
@@ -3379,7 +3598,7 @@ class ASTPattern extends ASTNode {
3379
3598
  return '##ERROR##';
3380
3599
 
3381
3600
  default:
3382
- return '#TODO pattern';
3601
+ throw new Error(`ASTPattern.codeFragment(): unsupported pattern type '${this.m_type}'`);
3383
3602
  }
3384
3603
  }
3385
3604
 
@@ -3412,6 +3631,7 @@ module.exports = {
3412
3631
  ASTList,
3413
3632
  ASTSet,
3414
3633
  ASTMap,
3634
+ ASTMapUnpack,
3415
3635
  ASTKwNamesMap,
3416
3636
  ASTConstMap,
3417
3637
  ASTSubscr,
@@ -3425,6 +3645,7 @@ module.exports = {
3425
3645
  ASTIterBlock,
3426
3646
  ASTContainerBlock,
3427
3647
  ASTWithBlock,
3648
+ ASTAsyncWithBlock,
3428
3649
  ASTComprehension,
3429
3650
  ASTLoadBuildClass,
3430
3651
  ASTAwaitable,
@@ -47,7 +47,7 @@ const opcodes = [
47
47
  [68, new OpCode(OpCodes.GET_ITER, "GET_ITER")],
48
48
  [69, new OpCode(OpCodes.STORE_LOCALS, "STORE_LOCALS")],
49
49
  [70, new OpCode(OpCodes.PRINT_EXPR, "PRINT_EXPR")],
50
- [71, new OpCode(OpCodes.PRINT_ITEM, "PRINT_ITEM")],
50
+ [71, new OpCode(OpCodes.LOAD_BUILD_CLASS, "LOAD_BUILD_CLASS")],
51
51
 
52
52
  [75, new OpCode(OpCodes.INPLACE_LSHIFT, "INPLACE_LSHIFT")],
53
53
  [76, new OpCode(OpCodes.INPLACE_RSHIFT, "INPLACE_RSHIFT")],
@@ -134,7 +134,7 @@ const opcodes = [
134
134
  [173, new OpCode(OpCodes.CALL_INTRINSIC_1, "CALL_INTRINSIC_1", {HasArgument: true, HasIntrisic1: true})],
135
135
  [174, new OpCode(OpCodes.CALL_INTRINSIC_2, "CALL_INTRINSIC_2", {HasArgument: true, HasIntrisic2: true})],
136
136
  [175, new OpCode(OpCodes.LOAD_FROM_DICT_OR_GLOBALS_A, "LOAD_FROM_DICT_OR_GLOBALS", {HasArgument: true, HasName: true})],
137
- [176, new OpCode(OpCodes.LOAD_FROM_DICT_OR_DEREF_A, "LOAD_FROM_DICT_OR_DEREF", {HasArgument: true, HasName: true, HasLocal: true})],
137
+ [176, new OpCode(OpCodes.LOAD_FROM_DICT_OR_DEREF_A, "LOAD_FROM_DICT_OR_DEREF", {HasArgument: true, HasFree: true})],
138
138
 
139
139
  [237, new OpCode(OpCodes.INSTRUMENTED_LOAD_SUPER_ATTR_A, "INSTRUMENTED_LOAD_SUPER_ATTR", {HasArgument: true, HasName: true, HasFlags: true})],
140
140
  [238, new OpCode(OpCodes.INSTRUMENTED_POP_JUMP_IF_NONE_A, "INSTRUMENTED_POP_JUMP_IF_NONE", {HasArgument: true, HasJumpRelative: true})],