Skip to main content

sphinx_ultra/py/
expr.rs

1//! Python expression parsing with `ast.unparse`-normalized output.
2//!
3//! Sphinx renders parameter defaults and (pieces of) annotations by running
4//! them through `ast.parse` + `ast.unparse` (`sphinx/util/inspect.py` routes
5//! them through `ast_unparse`). `parse_py_expr` + [`unparse`] reproduce that
6//! round trip byte-for-byte for the expression subset the py domain needs;
7//! anything outside the subset — `lambda`, comprehensions, f-strings,
8//! walrus, `await`, `yield`, conditional expressions, comparisons, slices,
9//! `**kwargs` in calls, complex literals — is a [`PyExprError`], which
10//! routes callers into the same fallback paths Sphinx takes when
11//! `ast.parse` raises `SyntaxError`.
12//!
13//! Sphinx does not use one parse mode everywhere, so neither do we:
14//!
15//! * [`parse_py_expr`] is `ast.parse(s, mode='eval')` — the mode reached
16//!   through `signature_from_str`'s `def func(...)` wrapper, where a
17//!   default value sits in an expression slot. A top-level `*a` is a
18//!   `SyntaxError` there.
19//! * [`parse_py_expr_stmt`] is plain `ast.parse(s)` (exec), the mode
20//!   `_parse_annotation` uses (`_annotations.py:232`, `type_comments=True`).
21//!   A top-level `Starred` is a legal `Expr` statement there, so PEP 646
22//!   `*Ts` / `*tuple[int, ...]` annotations parse, and an empty (or
23//!   blank) source is `Module(body=[])` rather than an error.
24//! * [`parse_py_star_annotation`] is CPython's `star_annotation`
25//!   production (`'*' bitwise_or | expression`), the only grammar slot
26//!   where `*args: *Ts` is legal — i.e. the annotation of a var-positional
27//!   parameter, which `signature_from_str` hands to Sphinx as `'*Ts'`.
28//!
29//! Every normalization rule implemented here is pinned by the unit-test
30//! oracle battery below, generated with the REAL pinned toolchain
31//! (`uv run --python 3.12 --with 'sphinx==9.1.0' --with 'docutils==0.22.4'`),
32//! never from memory. The paren-placement logic is a faithful port of
33//! CPython 3.12 `ast._Unparser` (`Lib/ast.py`): its `_Precedence` table,
34//! `require_parens`, `items_view`, and the per-node visitors for the subset.
35//!
36//! Known, documented divergences (all vanishingly rare in signatures, and
37//! all *conservative* — we return `Err` and the caller falls back, we never
38//! print something different from `ast.unparse`):
39//!
40//! * `\N{...}` escapes need the Unicode name database (no new deps) → `Err`.
41//! * Lone-surrogate escapes (`'\ud800'`) cannot live in a Rust `String` →
42//!   `Err`.
43//! * Identifier characters are approximated as `char::is_alphabetic` + `_`
44//!   (start) and additionally Nd digits via the wave-3
45//!   [`crate::rst::digits`] tables (continue), instead of exact
46//!   `XID_Start`/`XID_Continue`; identifiers are NFKC-normalized like
47//!   CPython's tokenizer.
48//! * `repr()`'s "printable" test for exotic non-ASCII characters inside
49//!   string constants uses `char::is_control` plus a curated Zs/Zl/Zp/Cf/Co
50//!   table rather than full Unicode category data. A full-codespace sweep
51//!   against the pinned interpreter shows the table never over-escapes, and
52//!   the only remaining under-escape class is the unassigned (Cn) code
53//!   points, which render unescaped where CPython would escape them — a
54//!   sanctioned, documented divergence.
55//! * Exec mode only ever yields ONE statement here: a source holding
56//!   several (`int;`, `int\nstr` — `Module(body=[Expr, Expr])`, which
57//!   Sphinx's `functools.reduce` over `node.body` renders as the
58//!   concatenation of both) is `Err`, so the caller falls back to a single
59//!   whole-text xref where Sphinx renders each statement.
60//! * [`MAX_DEPTH`] is a whole-expression complexity budget, not a nesting
61//!   depth: trailers (`a.b.c`, `f(x)(y)`, `m[i][j]`) and binop folds each
62//!   charge it too, so a FLAT chain of roughly 200 operations is `Err`
63//!   where CPython parses it. Err-side and conservative like the rest —
64//!   the caller falls back — and no realistic signature comes near it.
65
66use std::fmt;
67
68use unicode_normalization::UnicodeNormalization;
69
70use crate::rst::digits::decimal_digit_value;
71
72/// A parsed Python expression — the subset of `ast.expr` the py domain
73/// walks (task-3 brief). Negative literals are `UnaryOp(USub, Constant)`,
74/// exactly as in `ast`; dotted names are `Attribute` chains.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum PyExpr {
77    /// `ast.Name`.
78    Name(String),
79    /// `ast.Attribute`: `value.attr`.
80    Attribute(Box<PyExpr>, String),
81    /// `ast.Subscript`. `x[a, b]` stores the slice as a `Tuple`, `x[a]` as
82    /// the bare expression — mirroring `ast`.
83    Subscript {
84        value: Box<PyExpr>,
85        slice: Box<PyExpr>,
86    },
87    /// `ast.BinOp`, including `BitOr` for PEP 604 unions.
88    BinOp {
89        left: Box<PyExpr>,
90        op: PyOp,
91        right: Box<PyExpr>,
92    },
93    /// `ast.UnaryOp`.
94    UnaryOp { op: PyUnaryOp, operand: Box<PyExpr> },
95    /// `ast.Constant`.
96    Constant(PyConst),
97    /// `ast.Tuple`.
98    Tuple(Vec<PyExpr>),
99    /// `ast.List`.
100    List(Vec<PyExpr>),
101    /// `ast.Set` (never empty when produced by the parser: `{}` is a dict).
102    Set(Vec<PyExpr>),
103    /// `ast.Dict`; a `None` key is a `**` unpack (PEP 448).
104    Dict(Vec<(Option<PyExpr>, PyExpr)>),
105    /// `ast.Call`. `args` may contain `Starred` entries; `keywords` with
106    /// `arg=None` (`f(**kw)`) are unrepresentable and parse as `Err`.
107    Call {
108        func: Box<PyExpr>,
109        args: Vec<PyExpr>,
110        kwargs: Vec<(String, PyExpr)>,
111    },
112    /// `ast.Starred` — inside calls, displays and subscript tuples, plus
113    /// the two top-level slots exec mode and `star_annotation` open up
114    /// (see the module docs).
115    Starred(Box<PyExpr>),
116    /// `ast.BoolOp`: `a and b`, `a or b or c`. Chains of the *same*
117    /// operator flatten into one node, exactly as CPython's parser builds
118    /// them.
119    BoolOp { op: PyBoolOp, values: Vec<PyExpr> },
120}
121
122/// `ast.boolop`.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum PyBoolOp {
125    And,
126    Or,
127}
128
129/// `ast.BinOp` operators, i.e. every Python binary operator that is an
130/// `ast.operator` (comparisons are a different node kind and stay out of
131/// scope; `and`/`or` are [`PyBoolOp`]).
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum PyOp {
134    Add,
135    Sub,
136    Mult,
137    MatMult,
138    Div,
139    Mod,
140    Pow,
141    LShift,
142    RShift,
143    BitOr,
144    BitXor,
145    BitAnd,
146    FloorDiv,
147}
148
149/// `ast.unaryop`.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum PyUnaryOp {
152    Invert,
153    Not,
154    UAdd,
155    USub,
156}
157
158/// `ast.Constant` values. Numeric payloads are kept as **normalized
159/// strings** (what `repr(value)` prints), so arbitrary-precision ints
160/// survive and floats carry Python's shortest-repr text.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum PyConst {
163    None,
164    True,
165    False,
166    Ellipsis,
167    /// Normalized decimal digits, no sign, no underscores (`0xFF` → `255`).
168    Int(String),
169    /// Python `repr(float)` text (`1e-3` → `0.001`, `1e16` → `1e+16`).
170    Float(String),
171    /// A str constant. `value` is the *decoded* content; `quote` is the
172    /// quote character `repr()` chooses for it (recomputed by [`unparse`],
173    /// stored here so doctree consumers agree with the rendered text);
174    /// `u_prefix` preserves `ast.Constant.kind == 'u'` (`u'x'` unparses as
175    /// `u'x'`).
176    Str {
177        value: String,
178        quote: char,
179        u_prefix: bool,
180    },
181    /// A bytes constant: decoded bytes.
182    Bytes(Vec<u8>),
183}
184
185/// Opaque parse error; callers only branch on `Err` (the message exists for
186/// diagnostics and tests, not for dispatch).
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct PyExprError {
189    msg: String,
190}
191
192impl PyExprError {
193    fn new(msg: impl Into<String>) -> Self {
194        Self { msg: msg.into() }
195    }
196}
197
198impl fmt::Display for PyExprError {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        write!(f, "invalid python expression: {}", self.msg)
201    }
202}
203
204impl std::error::Error for PyExprError {}
205
206/// Parse a whole string as one Python expression (`ast.parse(s,
207/// mode='eval')` for the supported subset). Trailing garbage is an error;
208/// so is anything outside the subset. Never panics.
209pub fn parse_py_expr(s: &str) -> Result<PyExpr, PyExprError> {
210    let mut parser = Parser::new(tokenize(s)?, TopLevel::Eval);
211    if parser.peek().is_none() {
212        return Err(PyExprError::new("empty expression"));
213    }
214    parser.parse_top()
215}
216
217/// Parse a whole string the way `_parse_annotation` does — plain
218/// `ast.parse(s)`, i.e. exec mode (`_annotations.py:232`).
219///
220/// `Ok(None)` is `Module(body=[])`: an empty or blank source holds no
221/// statement at all, and Sphinx's `functools.reduce` over `node.body`
222/// then yields the empty node list (`_annotations.py:150-151`).
223/// `Ok(Some(e))` is the single `Expr` statement case, which is what every
224/// annotation in the subset is. A top-level `Starred` — bare (`*Ts`) or
225/// inside a bare tuple (`*a, b`) — is legal here and is *not* legal in
226/// [`parse_py_expr`].
227pub fn parse_py_expr_stmt(s: &str) -> Result<Option<PyExpr>, PyExprError> {
228    if has_leading_indent(s) {
229        // `ast.parse('  int')` raises IndentationError — a SyntaxError
230        // subclass, so `_parse_annotation` takes its `except SyntaxError`
231        // arm and xrefs the *unstripped* text.
232        return Err(PyExprError::new("unexpected indent"));
233    }
234    let mut parser = Parser::new(tokenize(s)?, TopLevel::Exec);
235    if parser.peek().is_none() {
236        return Ok(None);
237    }
238    parser.parse_top().map(Some)
239}
240
241/// Parse the annotation of a var-positional parameter — CPython's
242/// `star_annotation` production (`'*' bitwise_or | expression`), the only
243/// place a PEP 646 unpack may appear in a signature.
244pub fn parse_py_star_annotation(s: &str) -> Result<PyExpr, PyExprError> {
245    let mut parser = Parser::new(tokenize(s)?, TopLevel::StarAnnotation);
246    if parser.peek().is_none() {
247        return Err(PyExprError::new("empty expression"));
248    }
249    parser.parse_top()
250}
251
252/// `ast.parse`'s IndentationError test, narrowed to the shapes an
253/// annotation string can take: the first line that carries a token must
254/// not start with a space or a tab. A form feed is not indentation
255/// (`'\x0cint'` parses; `' \x0c int'` does not, because of the space), and
256/// wholly blank lines are skipped.
257fn has_leading_indent(s: &str) -> bool {
258    for line in s.split('\n') {
259        if line
260            .chars()
261            .all(|c| matches!(c, ' ' | '\t' | '\x0c' | '\r' | '\x0b'))
262        {
263            continue;
264        }
265        return line.starts_with([' ', '\t']);
266    }
267    false
268}
269
270/// Render an expression exactly as CPython 3.12 `ast.unparse` would render
271/// the equivalent `ast` tree (a port of `ast._Unparser` for the subset).
272pub fn unparse(e: &PyExpr) -> String {
273    let mut out = String::new();
274    // _Unparser.get_precedence defaults to _Precedence.TEST for any node
275    // that never had set_precedence called on it — the root included.
276    write_expr(&mut out, e, Prec::Test);
277    out
278}
279
280// ---------------------------------------------------------------------------
281// Tokenizer
282// ---------------------------------------------------------------------------
283
284/// Combined complexity budget: bounds active parser recursion (nesting)
285/// *and*, via [`Parser::charge_node`], the left-extending chains built
286/// iteratively (attribute/call/subscript trailers, binop folds). Together
287/// they guarantee any `Ok` tree has height O(`MAX_DEPTH`), keeping the
288/// recursive [`unparse`] walk and the derived recursive `Drop` of nested
289/// `Box<PyExpr>` stack-safe. Hostile input hits `Err` instead of a stack
290/// overflow (CPython raises `SyntaxError: too many nested parentheses`
291/// similarly).
292const MAX_DEPTH: u32 = 200;
293
294/// Python's hard keywords (`keyword.kwlist`, 3.12). Soft keywords
295/// (`match`, `case`, `type`, `_`) are ordinary names in expressions.
296const KEYWORDS: &[&str] = &[
297    "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue",
298    "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import",
299    "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while",
300    "with", "yield",
301];
302
303#[derive(Debug, Clone, PartialEq, Eq)]
304enum Tok {
305    Name(String),
306    /// Normalized decimal digits (see [`PyConst::Int`]).
307    Int(String),
308    /// Normalized `repr(float)` text (see [`PyConst::Float`]).
309    Float(String),
310    /// Decoded str-literal content.
311    Str {
312        value: String,
313        u_prefix: bool,
314    },
315    /// Decoded bytes-literal content.
316    Bytes(Vec<u8>),
317    LParen,
318    RParen,
319    LBracket,
320    RBracket,
321    LBrace,
322    RBrace,
323    Comma,
324    Colon,
325    Dot,
326    Ellipsis,
327    Eq,
328    Plus,
329    Minus,
330    Star,
331    DoubleStar,
332    Slash,
333    DoubleSlash,
334    Percent,
335    At,
336    Pipe,
337    Caret,
338    Amp,
339    Tilde,
340    LShift,
341    RShift,
342}
343
344/// Identifier start: `_` or `Alphabetic` — a conservative stand-in for
345/// `XID_Start` (module docs list the divergence).
346fn is_ident_start(c: char) -> bool {
347    c == '_' || c.is_alphabetic()
348}
349
350/// Identifier continue: start characters plus Nd digits, the latter via the
351/// wave-3 generated tables (`unicodedata.decimal`-defined = category Nd),
352/// so `x²` is rejected exactly like CPython ("invalid character '²'").
353fn is_ident_continue(c: char) -> bool {
354    is_ident_start(c) || decimal_digit_value(c).is_some()
355}
356
357struct Tokenizer {
358    chars: Vec<char>,
359    pos: usize,
360}
361
362impl Tokenizer {
363    fn cur(&self) -> Option<char> {
364        self.chars.get(self.pos).copied()
365    }
366
367    fn at(&self, i: usize) -> Option<char> {
368        self.chars.get(i).copied()
369    }
370
371    fn take_while(&mut self, pred: impl Fn(char) -> bool) -> String {
372        let mut out = String::new();
373        while let Some(c) = self.cur() {
374            if !pred(c) {
375                break;
376            }
377            out.push(c);
378            self.pos += 1;
379        }
380        out
381    }
382
383    /// Skip whitespace, comments and backslash line continuations. (Slightly
384    /// looser than CPython, which only allows ASCII whitespace; exotic
385    /// Unicode spaces are accepted here where CPython errors.)
386    fn skip_trivia(&mut self) {
387        loop {
388            match self.cur() {
389                Some(c) if c.is_whitespace() => self.pos += 1,
390                Some('#') => {
391                    while !matches!(self.cur(), None | Some('\n')) {
392                        self.pos += 1;
393                    }
394                }
395                Some('\\') if matches!(self.at(self.pos + 1), Some('\n' | '\r')) => {
396                    self.pos += 2;
397                }
398                _ => break,
399            }
400        }
401    }
402
403    fn scan_name_or_prefixed_string(&mut self) -> Result<Tok, PyExprError> {
404        let name = self.take_while(is_ident_continue);
405        if matches!(self.cur(), Some('\'' | '"')) {
406            match name.to_ascii_lowercase().as_str() {
407                "r" => return self.scan_string(true, false, false),
408                "b" => return self.scan_string(false, true, false),
409                "u" => return self.scan_string(false, false, true),
410                "rb" | "br" => return self.scan_string(true, true, false),
411                "f" | "rf" | "fr" => {
412                    return Err(PyExprError::new(
413                        "f-strings are not part of the supported expression subset",
414                    ));
415                }
416                // Any other identifier directly before a quote is invalid
417                // syntax in Python too; the parser reports the adjacency.
418                _ => {}
419            }
420        }
421        // CPython NFKC-normalizes identifiers (PEP 3131).
422        let name = if name.is_ascii() {
423            name
424        } else {
425            name.nfkc().collect()
426        };
427        Ok(Tok::Name(name))
428    }
429
430    fn scan_string(&mut self, raw: bool, bytes: bool, u_prefix: bool) -> Result<Tok, PyExprError> {
431        let Some(quote) = self.cur() else {
432            return Err(PyExprError::new("expected string quote"));
433        };
434        self.pos += 1;
435        let triple = self.cur() == Some(quote) && self.at(self.pos + 1) == Some(quote);
436        if triple {
437            self.pos += 2;
438        }
439        let mut body = String::new();
440        loop {
441            let Some(c) = self.cur() else {
442                return Err(PyExprError::new("unterminated string literal"));
443            };
444            if c == '\\' {
445                // Keep the escape pair raw; decoding happens below. In raw
446                // strings a backslash still shields a quote from
447                // terminating the literal (and both characters survive).
448                let Some(next) = self.at(self.pos + 1) else {
449                    return Err(PyExprError::new("unterminated string literal"));
450                };
451                body.push('\\');
452                body.push(next);
453                self.pos += 2;
454                continue;
455            }
456            if c == quote {
457                if !triple {
458                    self.pos += 1;
459                    break;
460                }
461                if self.at(self.pos + 1) == Some(quote) && self.at(self.pos + 2) == Some(quote) {
462                    self.pos += 3;
463                    break;
464                }
465            } else if c == '\n' && !triple {
466                return Err(PyExprError::new("EOL inside string literal"));
467            }
468            body.push(c);
469            self.pos += 1;
470        }
471        if bytes {
472            let value = if raw {
473                raw_bytes(&body)?
474            } else {
475                decode_bytes_escapes(&body)?
476            };
477            Ok(Tok::Bytes(value))
478        } else {
479            let value = if raw {
480                body
481            } else {
482                decode_str_escapes(&body)?
483            };
484            Ok(Tok::Str { value, u_prefix })
485        }
486    }
487
488    fn scan_number(&mut self) -> Result<Tok, PyExprError> {
489        if self.cur() == Some('0') {
490            let base = match self.at(self.pos + 1) {
491                Some('x' | 'X') => Some(16),
492                Some('o' | 'O') => Some(8),
493                Some('b' | 'B') => Some(2),
494                _ => None,
495            };
496            if let Some(base) = base {
497                self.pos += 2;
498                let run = self.take_while(|c| c == '_' || c.is_digit(base));
499                let digits = strip_underscores(&run, true)?;
500                if digits.is_empty() || self.cur().is_some_and(|c| c.is_ascii_digit()) {
501                    return Err(PyExprError::new("invalid digit in numeric literal"));
502                }
503                return Ok(Tok::Int(based_digits_to_decimal(&digits, base)));
504            }
505        }
506        let int_run = self.take_while(|c| c.is_ascii_digit() || c == '_');
507        let int_digits = strip_underscores(&int_run, false)?;
508        let mut is_float = false;
509        let mut frac_digits = String::new();
510        if self.cur() == Some('.') {
511            is_float = true;
512            self.pos += 1;
513            let frac_run = self.take_while(|c| c.is_ascii_digit() || c == '_');
514            frac_digits = strip_underscores(&frac_run, false)?;
515        }
516        let mut exp_part: Option<String> = None;
517        if matches!(self.cur(), Some('e' | 'E')) {
518            let mut look = self.pos + 1;
519            let mut negative = false;
520            if let Some(sign @ ('+' | '-')) = self.at(look) {
521                negative = sign == '-';
522                look += 1;
523            }
524            // Only a digit makes it an exponent; otherwise the `e` is the
525            // start of the next (invalid-here) identifier, as in CPython.
526            if self.at(look).is_some_and(|c| c.is_ascii_digit()) {
527                self.pos = look;
528                let run = self.take_while(|c| c.is_ascii_digit() || c == '_');
529                let digits = strip_underscores(&run, false)?;
530                is_float = true;
531                exp_part = Some(if negative {
532                    format!("-{digits}")
533                } else {
534                    digits
535                });
536            }
537        }
538        if matches!(self.cur(), Some('j' | 'J')) {
539            return Err(PyExprError::new(
540                "complex literals are not part of the supported expression subset",
541            ));
542        }
543        if is_float {
544            let mut text = String::new();
545            text.push_str(if int_digits.is_empty() {
546                "0"
547            } else {
548                &int_digits
549            });
550            text.push('.');
551            text.push_str(if frac_digits.is_empty() {
552                "0"
553            } else {
554                &frac_digits
555            });
556            if let Some(exp) = &exp_part {
557                text.push('e');
558                text.push_str(exp);
559            }
560            let value: f64 = text
561                .parse()
562                .map_err(|_| PyExprError::new("invalid float literal"))?;
563            return Ok(Tok::Float(py_float_repr(value)));
564        }
565        if int_digits.is_empty() {
566            return Err(PyExprError::new("invalid numeric literal"));
567        }
568        if int_digits.len() > 1
569            && int_digits.starts_with('0')
570            && int_digits.bytes().any(|b| b != b'0')
571        {
572            return Err(PyExprError::new(
573                "leading zeros in decimal integer literals are not permitted",
574            ));
575        }
576        let trimmed = int_digits.trim_start_matches('0');
577        Ok(Tok::Int(if trimmed.is_empty() {
578            "0".to_string()
579        } else {
580            trimmed.to_string()
581        }))
582    }
583
584    fn scan_operator(&mut self) -> Result<Tok, PyExprError> {
585        let Some(c) = self.cur() else {
586            return Err(PyExprError::new("unexpected end of input"));
587        };
588        let next = self.at(self.pos + 1);
589        let (tok, len) = match (c, next) {
590            ('*', Some('*')) => (Tok::DoubleStar, 2),
591            ('/', Some('/')) => (Tok::DoubleSlash, 2),
592            ('<', Some('<')) => (Tok::LShift, 2),
593            ('>', Some('>')) => (Tok::RShift, 2),
594            ('<' | '>', _) | ('=', Some('=')) | ('!', Some('=')) => {
595                return Err(PyExprError::new(
596                    "comparison operators are not part of the supported expression subset",
597                ));
598            }
599            ('(', _) => (Tok::LParen, 1),
600            (')', _) => (Tok::RParen, 1),
601            ('[', _) => (Tok::LBracket, 1),
602            (']', _) => (Tok::RBracket, 1),
603            ('{', _) => (Tok::LBrace, 1),
604            ('}', _) => (Tok::RBrace, 1),
605            (',', _) => (Tok::Comma, 1),
606            (':', _) => (Tok::Colon, 1),
607            ('=', _) => (Tok::Eq, 1),
608            ('+', _) => (Tok::Plus, 1),
609            ('-', _) => (Tok::Minus, 1),
610            ('*', _) => (Tok::Star, 1),
611            ('/', _) => (Tok::Slash, 1),
612            ('%', _) => (Tok::Percent, 1),
613            ('@', _) => (Tok::At, 1),
614            ('|', _) => (Tok::Pipe, 1),
615            ('^', _) => (Tok::Caret, 1),
616            ('&', _) => (Tok::Amp, 1),
617            ('~', _) => (Tok::Tilde, 1),
618            _ => {
619                return Err(PyExprError::new(format!(
620                    "unsupported character {c:?} in expression"
621                )));
622            }
623        };
624        self.pos += len;
625        Ok(tok)
626    }
627}
628
629fn tokenize(src: &str) -> Result<Vec<Tok>, PyExprError> {
630    let mut t = Tokenizer {
631        chars: src.chars().collect(),
632        pos: 0,
633    };
634    let mut toks = Vec::new();
635    loop {
636        t.skip_trivia();
637        let Some(c) = t.cur() else { break };
638        let tok = if is_ident_start(c) {
639            t.scan_name_or_prefixed_string()?
640        } else if c.is_ascii_digit() {
641            t.scan_number()?
642        } else if c == '\'' || c == '"' {
643            t.scan_string(false, false, false)?
644        } else if c == '.' {
645            if t.at(t.pos + 1) == Some('.') && t.at(t.pos + 2) == Some('.') {
646                t.pos += 3;
647                Tok::Ellipsis
648            } else if t.at(t.pos + 1).is_some_and(|d| d.is_ascii_digit()) {
649                t.scan_number()?
650            } else {
651                t.pos += 1;
652                Tok::Dot
653            }
654        } else {
655            t.scan_operator()?
656        };
657        toks.push(tok);
658    }
659    Ok(toks)
660}
661
662/// Validate PEP 515 underscore placement and strip them. `allow_leading`
663/// is the base-prefix case (`0x_FF` is legal, `0x__FF`/`0x_` are not).
664fn strip_underscores(run: &str, allow_leading: bool) -> Result<String, PyExprError> {
665    let mut out = String::with_capacity(run.len());
666    let mut last_was_underscore = false;
667    for (i, c) in run.chars().enumerate() {
668        if c == '_' {
669            let legal = if i == 0 {
670                allow_leading
671            } else {
672                !last_was_underscore
673            };
674            if !legal {
675                return Err(PyExprError::new("invalid underscore in numeric literal"));
676            }
677            last_was_underscore = true;
678        } else {
679            out.push(c);
680            last_was_underscore = false;
681        }
682    }
683    if last_was_underscore {
684        return Err(PyExprError::new("invalid underscore in numeric literal"));
685    }
686    Ok(out)
687}
688
689/// Convert base-2/8/16 digits to decimal text with unbounded precision
690/// (multiply-and-add over a little-endian decimal digit vector), because
691/// `repr(int)` — hence `ast.unparse` — prints every literal in decimal.
692fn based_digits_to_decimal(digits: &str, base: u32) -> String {
693    let mut dec: Vec<u8> = vec![0];
694    for c in digits.chars() {
695        let mut carry = c.to_digit(base).unwrap_or(0);
696        for slot in dec.iter_mut() {
697            let v = u32::from(*slot) * base + carry;
698            *slot = (v % 10) as u8;
699            carry = v / 10;
700        }
701        while carry > 0 {
702            dec.push((carry % 10) as u8);
703            carry /= 10;
704        }
705    }
706    dec.iter().rev().map(|d| char::from(b'0' + d)).collect()
707}
708
709/// Python `repr(float)` (shortest round trip + `%.17g`-style placement:
710/// fixed notation iff the decimal exponent is in `-4..16`, else scientific
711/// with a signed, two-digit-minimum exponent). Infinities — only reachable
712/// via overflowing literals like `1e400` — print as `ast._Unparser`'s
713/// `_INFSTR`, `1e309`. Callers pass magnitudes only: a Python float
714/// literal has no sign (negatives are `UnaryOp`), and the digit-placement
715/// arithmetic below is only correct for non-negative inputs.
716fn py_float_repr(v: f64) -> String {
717    debug_assert!(
718        v >= 0.0 || v.is_nan(),
719        "py_float_repr takes magnitudes only"
720    );
721    if v.is_infinite() {
722        return "1e309".to_string();
723    }
724    // Rust's LowerExp is shortest-round-trip, same as CPython repr digits.
725    let sci = format!("{v:e}");
726    let (mantissa, exp_text) = sci.split_once('e').unwrap_or((sci.as_str(), "0"));
727    let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
728    let exp10: i32 = exp_text.parse().unwrap_or(0);
729    if (-4..16).contains(&exp10) {
730        if exp10 >= 0 {
731            let int_len = exp10.unsigned_abs() as usize + 1;
732            if digits.len() > int_len {
733                format!("{}.{}", &digits[..int_len], &digits[int_len..])
734            } else {
735                let zeros = "0".repeat(int_len - digits.len());
736                format!("{digits}{zeros}.0")
737            }
738        } else {
739            let zeros = "0".repeat(exp10.unsigned_abs() as usize - 1);
740            format!("0.{zeros}{digits}")
741        }
742    } else {
743        let mantissa_out = if digits.len() == 1 {
744            digits
745        } else {
746            format!("{}.{}", &digits[..1], &digits[1..])
747        };
748        let sign = if exp10 < 0 { '-' } else { '+' };
749        format!("{mantissa_out}e{sign}{:02}", exp10.unsigned_abs())
750    }
751}
752
753fn take_hex(chars: &[char], i: &mut usize, n: usize, kind: &str) -> Result<u32, PyExprError> {
754    let mut val: u32 = 0;
755    for _ in 0..n {
756        let digit = chars.get(*i).and_then(|c| c.to_digit(16));
757        let Some(digit) = digit else {
758            return Err(PyExprError::new(format!("truncated {kind} escape")));
759        };
760        val = val * 16 + digit;
761        *i += 1;
762    }
763    Ok(val)
764}
765
766fn take_octal(chars: &[char], i: &mut usize, first: char) -> u32 {
767    let mut val = first.to_digit(8).unwrap_or(0);
768    for _ in 0..2 {
769        let Some(digit) = chars.get(*i).and_then(|c| c.to_digit(8)) else {
770            break;
771        };
772        val = val * 8 + digit;
773        *i += 1;
774    }
775    val
776}
777
778/// CPython str-literal escape decoding. Unknown escapes keep the backslash
779/// literally (CPython emits a `SyntaxWarning` but accepts them); `\N{...}`
780/// needs the Unicode name database and is a documented `Err`; surrogate
781/// `\u`/`\U` values cannot exist in a Rust `String` and are `Err` too.
782fn decode_str_escapes(body: &str) -> Result<String, PyExprError> {
783    let chars: Vec<char> = body.chars().collect();
784    let mut out = String::with_capacity(body.len());
785    let mut i = 0;
786    while i < chars.len() {
787        let Some(&c) = chars.get(i) else { break };
788        if c != '\\' {
789            out.push(c);
790            i += 1;
791            continue;
792        }
793        let Some(&esc) = chars.get(i + 1) else {
794            return Err(PyExprError::new("trailing backslash in string literal"));
795        };
796        i += 2;
797        match esc {
798            '\n' => {}
799            '\\' => out.push('\\'),
800            '\'' => out.push('\''),
801            '"' => out.push('"'),
802            'a' => out.push('\u{7}'),
803            'b' => out.push('\u{8}'),
804            'f' => out.push('\u{c}'),
805            'n' => out.push('\n'),
806            'r' => out.push('\r'),
807            't' => out.push('\t'),
808            'v' => out.push('\u{b}'),
809            '0'..='7' => {
810                let val = take_octal(&chars, &mut i, esc);
811                // <= 0o777 = 511, always a valid scalar.
812                out.push(char::from_u32(val).unwrap_or('\u{fffd}'));
813            }
814            'x' => {
815                let val = take_hex(&chars, &mut i, 2, "\\xXX")?;
816                out.push(char::from_u32(val).unwrap_or('\u{fffd}'));
817            }
818            'u' => {
819                let val = take_hex(&chars, &mut i, 4, "\\uXXXX")?;
820                let Some(decoded) = char::from_u32(val) else {
821                    return Err(PyExprError::new("surrogate escapes are not supported"));
822                };
823                out.push(decoded);
824            }
825            'U' => {
826                let val = take_hex(&chars, &mut i, 8, "\\UXXXXXXXX")?;
827                let Some(decoded) = char::from_u32(val) else {
828                    return Err(PyExprError::new("invalid \\U escape value"));
829                };
830                out.push(decoded);
831            }
832            'N' => {
833                return Err(PyExprError::new(
834                    "\\N{...} escapes are not supported (no unicodedata)",
835                ));
836            }
837            other => {
838                out.push('\\');
839                out.push(other);
840            }
841        }
842    }
843    Ok(out)
844}
845
846/// Bytes-literal escape decoding. Literal characters must be ASCII
847/// (CPython: "bytes can only contain ASCII literal characters"); octal
848/// escape values wrap to a byte, matching CPython (`b'\401'` → `b'\x01'`).
849fn decode_bytes_escapes(body: &str) -> Result<Vec<u8>, PyExprError> {
850    let chars: Vec<char> = body.chars().collect();
851    let mut out = Vec::with_capacity(body.len());
852    let mut i = 0;
853    while i < chars.len() {
854        let Some(&c) = chars.get(i) else { break };
855        if c != '\\' {
856            if !c.is_ascii() {
857                return Err(PyExprError::new(
858                    "bytes can only contain ASCII literal characters",
859                ));
860            }
861            out.push(c as u8);
862            i += 1;
863            continue;
864        }
865        let Some(&esc) = chars.get(i + 1) else {
866            return Err(PyExprError::new("trailing backslash in bytes literal"));
867        };
868        i += 2;
869        match esc {
870            '\n' => {}
871            '\\' => out.push(b'\\'),
872            '\'' => out.push(b'\''),
873            '"' => out.push(b'"'),
874            'a' => out.push(0x07),
875            'b' => out.push(0x08),
876            'f' => out.push(0x0c),
877            'n' => out.push(b'\n'),
878            'r' => out.push(b'\r'),
879            't' => out.push(b'\t'),
880            'v' => out.push(0x0b),
881            '0'..='7' => out.push(take_octal(&chars, &mut i, esc) as u8),
882            'x' => out.push(take_hex(&chars, &mut i, 2, "\\xXX")? as u8),
883            // \u, \U, \N are not escapes in bytes literals: the backslash
884            // stays literal, like any other unknown escape.
885            other => {
886                if !other.is_ascii() {
887                    return Err(PyExprError::new(
888                        "bytes can only contain ASCII literal characters",
889                    ));
890                }
891                out.push(b'\\');
892                out.push(other as u8);
893            }
894        }
895    }
896    Ok(out)
897}
898
899fn raw_bytes(body: &str) -> Result<Vec<u8>, PyExprError> {
900    if !body.is_ascii() {
901        return Err(PyExprError::new(
902            "bytes can only contain ASCII literal characters",
903        ));
904    }
905    Ok(body.bytes().collect())
906}
907
908// ---------------------------------------------------------------------------
909// Parser (recursive descent over Python's expression precedence ladder)
910// ---------------------------------------------------------------------------
911//
912// Grammar subset, loosest to tightest (the real Python levels for the ops
913// we support; excluded levels — comparisons, conditional expressions,
914// lambda — are `Err`):
915//
916//   top      := star_or_expr (',' star_or_expr)* [',']       (bare tuple)
917//   expr     := bool_and ('or' bool_and)*
918//   bool_and := not_expr ('and' not_expr)*
919//   not_expr := 'not' not_expr | bitor
920//   bitor    := bitxor ('|' bitxor)*
921//   bitxor   := bitand ('^' bitand)*
922//   bitand   := shift ('&' shift)*
923//   shift    := arith (('<<' | '>>') arith)*
924//   arith    := term (('+' | '-') term)*
925//   term     := factor (('*' | '/' | '//' | '%' | '@') factor)*
926//   factor   := ('+' | '-' | '~') factor | power
927//   power    := postfix ['**' factor]                        (right assoc)
928//   postfix  := atom ('.' NAME | '(' args ')' | '[' items ']')*
929//   star_or_expr := '*' bitor | expr                         (PEP 448/646)
930
931/// Which of `ast.parse`'s grammar entry points the *top level* of this
932/// parse follows. Nothing below the top level differs between them.
933#[derive(Debug, Clone, Copy, PartialEq, Eq)]
934enum TopLevel {
935    /// `mode='eval'`: no starred expression at the top level, not even
936    /// inside a bare tuple (`*a`, `*a, b`, `*a,` are all SyntaxError).
937    Eval,
938    /// Plain `ast.parse` (exec): the top level is an `Expr` statement, so
939    /// `*a` and `*a, b` are legal.
940    Exec,
941    /// `star_annotation` (`'*' bitwise_or | expression`): one optional
942    /// leading `*`, never a starred tuple element.
943    StarAnnotation,
944}
945
946struct Parser {
947    toks: Vec<Tok>,
948    pos: usize,
949    depth: u32,
950    top: TopLevel,
951}
952
953/// The binary operator accepted at each precedence level of `parse_binop`.
954fn level_op(level: usize, tok: &Tok) -> Option<PyOp> {
955    match (level, tok) {
956        (0, Tok::Pipe) => Some(PyOp::BitOr),
957        (1, Tok::Caret) => Some(PyOp::BitXor),
958        (2, Tok::Amp) => Some(PyOp::BitAnd),
959        (3, Tok::LShift) => Some(PyOp::LShift),
960        (3, Tok::RShift) => Some(PyOp::RShift),
961        (4, Tok::Plus) => Some(PyOp::Add),
962        (4, Tok::Minus) => Some(PyOp::Sub),
963        (5, Tok::Star) => Some(PyOp::Mult),
964        (5, Tok::Slash) => Some(PyOp::Div),
965        (5, Tok::DoubleSlash) => Some(PyOp::FloorDiv),
966        (5, Tok::Percent) => Some(PyOp::Mod),
967        (5, Tok::At) => Some(PyOp::MatMult),
968        _ => None,
969    }
970}
971
972impl Parser {
973    fn new(toks: Vec<Tok>, top: TopLevel) -> Self {
974        Self {
975            toks,
976            pos: 0,
977            depth: 0,
978            top,
979        }
980    }
981
982    fn peek(&self) -> Option<&Tok> {
983        self.toks.get(self.pos)
984    }
985
986    /// `and` / `or` / `not` lex as `Tok::Name`; this is the keyword test
987    /// the boolean levels branch on.
988    fn peek_keyword(&self, kw: &str) -> bool {
989        matches!(self.peek(), Some(Tok::Name(n)) if n == kw)
990    }
991
992    fn eat_keyword(&mut self, kw: &str) -> bool {
993        if self.peek_keyword(kw) {
994            self.pos += 1;
995            true
996        } else {
997            false
998        }
999    }
1000
1001    fn eat(&mut self, t: &Tok) -> bool {
1002        if self.peek() == Some(t) {
1003            self.pos += 1;
1004            true
1005        } else {
1006            false
1007        }
1008    }
1009
1010    fn expect(&mut self, t: &Tok, what: &str) -> Result<(), PyExprError> {
1011        if self.eat(t) {
1012            Ok(())
1013        } else {
1014            Err(PyExprError::new(format!("expected {what}")))
1015        }
1016    }
1017
1018    /// Charge one unit of the [`MAX_DEPTH`] budget for a node built by an
1019    /// *iterative* loop (postfix trailers, binop folds). Unlike the
1020    /// recursion guards in `parse_expr`/`parse_factor` this charge is never
1021    /// refunded: those loops deepen the tree without deepening the parser
1022    /// stack, so `a` + `.b` × N would otherwise return an `Ok` tree whose
1023    /// recursive `unparse`/`Drop` aborts the process. The permanent charge
1024    /// makes `MAX_DEPTH` a whole-expression complexity budget that bounds
1025    /// the height of every `Ok` tree.
1026    fn charge_node(&mut self) -> Result<(), PyExprError> {
1027        self.depth += 1;
1028        if self.depth > MAX_DEPTH {
1029            return Err(PyExprError::new("expression is too deeply nested"));
1030        }
1031        Ok(())
1032    }
1033
1034    fn parse_top(&mut self) -> Result<PyExpr, PyExprError> {
1035        let first = self.parse_star_or_expr()?;
1036        let mut elts = vec![first];
1037        let mut tuple = false;
1038        while self.eat(&Tok::Comma) {
1039            tuple = true;
1040            if self.peek().is_none() {
1041                break;
1042            }
1043            elts.push(self.parse_star_or_expr()?);
1044        }
1045        if self.peek().is_some() {
1046            return Err(PyExprError::new("unexpected trailing input"));
1047        }
1048        // `ast.parse(mode='eval')` rejects starred expressions at the top
1049        // level even inside a bare tuple (`*a, b`, `b, *a`, `*a,` are all
1050        // SyntaxError, oracle-verified) — only displays, calls and
1051        // subscripts take them. Exec mode (what `_parse_annotation` uses)
1052        // and `star_annotation` do accept them; see the module docs.
1053        let starred = elts.iter().any(|e| matches!(e, PyExpr::Starred(_)));
1054        let star_ok = match self.top {
1055            TopLevel::Eval => false,
1056            TopLevel::Exec => true,
1057            TopLevel::StarAnnotation => !tuple,
1058        };
1059        if starred && !star_ok {
1060            return Err(PyExprError::new("cannot use starred expression here"));
1061        }
1062        if tuple {
1063            return Ok(PyExpr::Tuple(elts));
1064        }
1065        match elts.into_iter().next() {
1066            Some(e) => Ok(e),
1067            None => Err(PyExprError::new("empty expression")),
1068        }
1069    }
1070
1071    /// `'*' bitor | expr` — starred items are only reachable from the
1072    /// display/call/subscript element positions that call this.
1073    fn parse_star_or_expr(&mut self) -> Result<PyExpr, PyExprError> {
1074        if self.eat(&Tok::Star) {
1075            Ok(PyExpr::Starred(Box::new(self.parse_binop(0)?)))
1076        } else {
1077            self.parse_expr()
1078        }
1079    }
1080
1081    fn parse_expr(&mut self) -> Result<PyExpr, PyExprError> {
1082        self.depth += 1;
1083        if self.depth > MAX_DEPTH {
1084            self.depth -= 1;
1085            return Err(PyExprError::new("expression is too deeply nested"));
1086        }
1087        let result = self.parse_expr_inner();
1088        self.depth -= 1;
1089        result
1090    }
1091
1092    fn parse_expr_inner(&mut self) -> Result<PyExpr, PyExprError> {
1093        self.parse_bool_op(PyBoolOp::Or)
1094    }
1095
1096    /// `or` and `and` share one shape; `Or` recurses into `And`, `And` into
1097    /// `not_expr`. A chain of the same operator flattens into one
1098    /// `ast.BoolOp` with N values, as CPython's parser builds it.
1099    fn parse_bool_op(&mut self, op: PyBoolOp) -> Result<PyExpr, PyExprError> {
1100        let kw = match op {
1101            PyBoolOp::Or => "or",
1102            PyBoolOp::And => "and",
1103        };
1104        let first = self.parse_bool_operand(op)?;
1105        if !self.peek_keyword(kw) {
1106            return Ok(first);
1107        }
1108        let mut values = vec![first];
1109        while self.eat_keyword(kw) {
1110            self.charge_node()?;
1111            values.push(self.parse_bool_operand(op)?);
1112        }
1113        Ok(PyExpr::BoolOp { op, values })
1114    }
1115
1116    fn parse_bool_operand(&mut self, op: PyBoolOp) -> Result<PyExpr, PyExprError> {
1117        match op {
1118            PyBoolOp::Or => self.parse_bool_op(PyBoolOp::And),
1119            PyBoolOp::And => self.parse_not(),
1120        }
1121    }
1122
1123    /// `not_expr := 'not' not_expr | bitor`. `not` binds tighter than
1124    /// `and`/`or`, so `not a or b` is `(not a) or b`.
1125    fn parse_not(&mut self) -> Result<PyExpr, PyExprError> {
1126        if !self.peek_keyword("not") {
1127            return self.parse_binop(0);
1128        }
1129        self.depth += 1;
1130        if self.depth > MAX_DEPTH {
1131            self.depth -= 1;
1132            return Err(PyExprError::new("expression is too deeply nested"));
1133        }
1134        self.pos += 1;
1135        let operand = self.parse_not();
1136        self.depth -= 1;
1137        Ok(PyExpr::UnaryOp {
1138            op: PyUnaryOp::Not,
1139            operand: Box::new(operand?),
1140        })
1141    }
1142
1143    fn parse_binop(&mut self, level: usize) -> Result<PyExpr, PyExprError> {
1144        if level == 6 {
1145            return self.parse_factor();
1146        }
1147        let mut left = self.parse_binop(level + 1)?;
1148        while let Some(op) = self.peek().and_then(|t| level_op(level, t)) {
1149            self.pos += 1;
1150            self.charge_node()?;
1151            let right = self.parse_binop(level + 1)?;
1152            left = PyExpr::BinOp {
1153                left: Box::new(left),
1154                op,
1155                right: Box::new(right),
1156            };
1157        }
1158        Ok(left)
1159    }
1160
1161    fn parse_factor(&mut self) -> Result<PyExpr, PyExprError> {
1162        self.depth += 1;
1163        if self.depth > MAX_DEPTH {
1164            self.depth -= 1;
1165            return Err(PyExprError::new("expression is too deeply nested"));
1166        }
1167        let result = self.parse_factor_inner();
1168        self.depth -= 1;
1169        result
1170    }
1171
1172    fn parse_factor_inner(&mut self) -> Result<PyExpr, PyExprError> {
1173        let op = match self.peek() {
1174            Some(Tok::Plus) => Some(PyUnaryOp::UAdd),
1175            Some(Tok::Minus) => Some(PyUnaryOp::USub),
1176            Some(Tok::Tilde) => Some(PyUnaryOp::Invert),
1177            _ => None,
1178        };
1179        if let Some(op) = op {
1180            self.pos += 1;
1181            let operand = self.parse_factor()?;
1182            return Ok(PyExpr::UnaryOp {
1183                op,
1184                operand: Box::new(operand),
1185            });
1186        }
1187        self.parse_power()
1188    }
1189
1190    fn parse_power(&mut self) -> Result<PyExpr, PyExprError> {
1191        let base = self.parse_postfix()?;
1192        if self.eat(&Tok::DoubleStar) {
1193            // Right-hand side is a factor: `2 ** -x ** 3` nests rightward.
1194            let right = self.parse_factor()?;
1195            return Ok(PyExpr::BinOp {
1196                left: Box::new(base),
1197                op: PyOp::Pow,
1198                right: Box::new(right),
1199            });
1200        }
1201        Ok(base)
1202    }
1203
1204    fn parse_postfix(&mut self) -> Result<PyExpr, PyExprError> {
1205        let mut e = self.parse_atom()?;
1206        loop {
1207            if !matches!(self.peek(), Some(Tok::Dot | Tok::LParen | Tok::LBracket)) {
1208                return Ok(e);
1209            }
1210            self.charge_node()?;
1211            if self.eat(&Tok::Dot) {
1212                let attr = match self.peek() {
1213                    Some(Tok::Name(n)) => n.clone(),
1214                    _ => return Err(PyExprError::new("expected attribute name after '.'")),
1215                };
1216                if KEYWORDS.contains(&attr.as_str()) {
1217                    return Err(PyExprError::new("keyword cannot be an attribute name"));
1218                }
1219                self.pos += 1;
1220                e = PyExpr::Attribute(Box::new(e), attr);
1221            } else if self.eat(&Tok::LParen) {
1222                e = self.parse_call(e)?;
1223            } else if self.eat(&Tok::LBracket) {
1224                e = self.parse_subscript(e)?;
1225            } else {
1226                return Ok(e);
1227            }
1228        }
1229    }
1230
1231    fn parse_atom(&mut self) -> Result<PyExpr, PyExprError> {
1232        let Some(tok) = self.peek() else {
1233            return Err(PyExprError::new("unexpected end of expression"));
1234        };
1235        match tok {
1236            Tok::Name(n) => {
1237                let name = n.clone();
1238                self.pos += 1;
1239                match name.as_str() {
1240                    "True" => Ok(PyExpr::Constant(PyConst::True)),
1241                    "False" => Ok(PyExpr::Constant(PyConst::False)),
1242                    "None" => Ok(PyExpr::Constant(PyConst::None)),
1243                    _ if KEYWORDS.contains(&name.as_str()) => Err(PyExprError::new(format!(
1244                        "keyword {name:?} is not part of the supported expression subset"
1245                    ))),
1246                    _ => Ok(PyExpr::Name(name)),
1247                }
1248            }
1249            Tok::Int(digits) => {
1250                let digits = digits.clone();
1251                self.pos += 1;
1252                Ok(PyExpr::Constant(PyConst::Int(digits)))
1253            }
1254            Tok::Float(text) => {
1255                let text = text.clone();
1256                self.pos += 1;
1257                Ok(PyExpr::Constant(PyConst::Float(text)))
1258            }
1259            Tok::Str { .. } | Tok::Bytes(_) => self.parse_string_concat(),
1260            Tok::Ellipsis => {
1261                self.pos += 1;
1262                Ok(PyExpr::Constant(PyConst::Ellipsis))
1263            }
1264            Tok::LParen => {
1265                self.pos += 1;
1266                self.parse_paren()
1267            }
1268            Tok::LBracket => {
1269                self.pos += 1;
1270                self.parse_list()
1271            }
1272            Tok::LBrace => {
1273                self.pos += 1;
1274                self.parse_brace()
1275            }
1276            other => Err(PyExprError::new(format!("unexpected token {other:?}"))),
1277        }
1278    }
1279
1280    /// Adjacent string-literal concatenation. The `u` kind follows the
1281    /// first piece (`u'a' 'b'` → `u'ab'`, `'a' u'b'` → `'ab'` — oracle);
1282    /// mixing bytes and str is the same `SyntaxError` CPython raises.
1283    fn parse_string_concat(&mut self) -> Result<PyExpr, PyExprError> {
1284        enum Acc {
1285            Str { value: String, u_prefix: bool },
1286            Bytes(Vec<u8>),
1287        }
1288        let mut acc = match self.peek() {
1289            Some(Tok::Str { value, u_prefix }) => Acc::Str {
1290                value: value.clone(),
1291                u_prefix: *u_prefix,
1292            },
1293            Some(Tok::Bytes(b)) => Acc::Bytes(b.clone()),
1294            _ => return Err(PyExprError::new("expected string literal")),
1295        };
1296        self.pos += 1;
1297        loop {
1298            match (self.peek(), &mut acc) {
1299                (Some(Tok::Str { value, .. }), Acc::Str { value: v, .. }) => {
1300                    v.push_str(value);
1301                    self.pos += 1;
1302                }
1303                (Some(Tok::Bytes(b)), Acc::Bytes(v)) => {
1304                    v.extend_from_slice(b);
1305                    self.pos += 1;
1306                }
1307                (Some(Tok::Str { .. }), Acc::Bytes(_)) | (Some(Tok::Bytes(_)), Acc::Str { .. }) => {
1308                    return Err(PyExprError::new("cannot mix bytes and nonbytes literals"));
1309                }
1310                _ => break,
1311            }
1312        }
1313        Ok(match acc {
1314            Acc::Str { value, u_prefix } => {
1315                let quote = repr_quote(&value);
1316                PyExpr::Constant(PyConst::Str {
1317                    value,
1318                    quote,
1319                    u_prefix,
1320                })
1321            }
1322            Acc::Bytes(b) => PyExpr::Constant(PyConst::Bytes(b)),
1323        })
1324    }
1325
1326    fn parse_paren(&mut self) -> Result<PyExpr, PyExprError> {
1327        if self.eat(&Tok::RParen) {
1328            return Ok(PyExpr::Tuple(Vec::new()));
1329        }
1330        let first = self.parse_star_or_expr()?;
1331        let mut elts = vec![first];
1332        let mut tuple = false;
1333        while self.eat(&Tok::Comma) {
1334            tuple = true;
1335            if self.peek() == Some(&Tok::RParen) {
1336                break;
1337            }
1338            elts.push(self.parse_star_or_expr()?);
1339        }
1340        self.expect(&Tok::RParen, "')'")?;
1341        if tuple {
1342            return Ok(PyExpr::Tuple(elts));
1343        }
1344        match elts.into_iter().next() {
1345            // `(*a)` without a comma is CPython's "can't use starred
1346            // expression here".
1347            Some(PyExpr::Starred(_)) => Err(PyExprError::new("cannot use starred expression here")),
1348            // Plain parentheses group; they leave no node behind.
1349            Some(e) => Ok(e),
1350            None => Err(PyExprError::new("empty parentheses")),
1351        }
1352    }
1353
1354    fn parse_list(&mut self) -> Result<PyExpr, PyExprError> {
1355        let mut elts = Vec::new();
1356        if !self.eat(&Tok::RBracket) {
1357            loop {
1358                elts.push(self.parse_star_or_expr()?);
1359                if self.eat(&Tok::Comma) {
1360                    if self.eat(&Tok::RBracket) {
1361                        break;
1362                    }
1363                    continue;
1364                }
1365                self.expect(&Tok::RBracket, "']'")?;
1366                break;
1367            }
1368        }
1369        Ok(PyExpr::List(elts))
1370    }
1371
1372    fn parse_brace(&mut self) -> Result<PyExpr, PyExprError> {
1373        if self.eat(&Tok::RBrace) {
1374            return Ok(PyExpr::Dict(Vec::new()));
1375        }
1376        if self.peek() == Some(&Tok::DoubleStar) {
1377            return self.parse_dict_items(None);
1378        }
1379        let first = self.parse_star_or_expr()?;
1380        if !matches!(first, PyExpr::Starred(_)) && self.peek() == Some(&Tok::Colon) {
1381            return self.parse_dict_items(Some(first));
1382        }
1383        self.parse_set_items(first)
1384    }
1385
1386    fn parse_dict_items(&mut self, first_key: Option<PyExpr>) -> Result<PyExpr, PyExprError> {
1387        let mut items = Vec::new();
1388        if let Some(key) = first_key {
1389            self.expect(&Tok::Colon, "':'")?;
1390            items.push((Some(key), self.parse_expr()?));
1391        } else {
1392            self.expect(&Tok::DoubleStar, "'**'")?;
1393            // `'**' or_expr` — the unpacked value sits at bitor level.
1394            items.push((None, self.parse_binop(0)?));
1395        }
1396        loop {
1397            if self.eat(&Tok::Comma) {
1398                if self.eat(&Tok::RBrace) {
1399                    break;
1400                }
1401                if self.eat(&Tok::DoubleStar) {
1402                    items.push((None, self.parse_binop(0)?));
1403                } else {
1404                    let key = self.parse_expr()?;
1405                    self.expect(&Tok::Colon, "':'")?;
1406                    items.push((Some(key), self.parse_expr()?));
1407                }
1408                continue;
1409            }
1410            self.expect(&Tok::RBrace, "'}'")?;
1411            break;
1412        }
1413        Ok(PyExpr::Dict(items))
1414    }
1415
1416    fn parse_set_items(&mut self, first: PyExpr) -> Result<PyExpr, PyExprError> {
1417        let mut elts = vec![first];
1418        loop {
1419            if self.eat(&Tok::Comma) {
1420                if self.eat(&Tok::RBrace) {
1421                    break;
1422                }
1423                elts.push(self.parse_star_or_expr()?);
1424                continue;
1425            }
1426            self.expect(&Tok::RBrace, "'}'")?;
1427            break;
1428        }
1429        Ok(PyExpr::Set(elts))
1430    }
1431
1432    /// Call arguments; the opening `(` is already consumed. `*args` may
1433    /// follow keywords (and `ast` reorders the rendering — `f(1, x=2, *a)`
1434    /// unparses as `f(1, *a, x=2)`); a plain positional after a keyword is
1435    /// CPython's "positional argument follows keyword argument"; `**kw` is
1436    /// unrepresentable in [`PyExpr::Call`] and therefore `Err`.
1437    fn parse_call(&mut self, func: PyExpr) -> Result<PyExpr, PyExprError> {
1438        let mut args = Vec::new();
1439        let mut kwargs: Vec<(String, PyExpr)> = Vec::new();
1440        if !self.eat(&Tok::RParen) {
1441            loop {
1442                if self.peek() == Some(&Tok::DoubleStar) {
1443                    return Err(PyExprError::new(
1444                        "** unpacking in a call is not representable",
1445                    ));
1446                }
1447                if self.eat(&Tok::Star) {
1448                    args.push(PyExpr::Starred(Box::new(self.parse_binop(0)?)));
1449                } else {
1450                    let e = self.parse_expr()?;
1451                    if self.peek() == Some(&Tok::Eq) {
1452                        let PyExpr::Name(name) = e else {
1453                            return Err(PyExprError::new(
1454                                "keyword argument name must be an identifier",
1455                            ));
1456                        };
1457                        self.pos += 1;
1458                        kwargs.push((name, self.parse_expr()?));
1459                    } else {
1460                        if !kwargs.is_empty() {
1461                            return Err(PyExprError::new(
1462                                "positional argument follows keyword argument",
1463                            ));
1464                        }
1465                        args.push(e);
1466                    }
1467                }
1468                if self.eat(&Tok::Comma) {
1469                    if self.eat(&Tok::RParen) {
1470                        break;
1471                    }
1472                    continue;
1473                }
1474                self.expect(&Tok::RParen, "')'")?;
1475                break;
1476            }
1477        }
1478        Ok(PyExpr::Call {
1479            func: Box::new(func),
1480            args,
1481            kwargs,
1482        })
1483    }
1484
1485    /// Subscript items; the opening `[` is already consumed. Mirrors `ast`:
1486    /// `x[a]` keeps the bare expression as the slice, any comma (or a lone
1487    /// starred item, PEP 646) makes it a `Tuple`. Slices (`:`) are outside
1488    /// the subset and fail here.
1489    fn parse_subscript(&mut self, value: PyExpr) -> Result<PyExpr, PyExprError> {
1490        if self.eat(&Tok::RBracket) {
1491            return Err(PyExprError::new("empty subscript"));
1492        }
1493        let first = self.parse_star_or_expr()?;
1494        let mut elts = vec![first];
1495        let mut tuple = false;
1496        while self.eat(&Tok::Comma) {
1497            tuple = true;
1498            if self.peek() == Some(&Tok::RBracket) {
1499                break;
1500            }
1501            elts.push(self.parse_star_or_expr()?);
1502        }
1503        self.expect(&Tok::RBracket, "']'")?;
1504        let slice = if tuple {
1505            PyExpr::Tuple(elts)
1506        } else {
1507            match elts.into_iter().next() {
1508                Some(starred @ PyExpr::Starred(_)) => PyExpr::Tuple(vec![starred]),
1509                Some(e) => e,
1510                None => return Err(PyExprError::new("empty subscript")),
1511            }
1512        };
1513        Ok(PyExpr::Subscript {
1514            value: Box::new(value),
1515            slice: Box::new(slice),
1516        })
1517    }
1518}
1519
1520// ---------------------------------------------------------------------------
1521// Unparser (port of CPython 3.12 ast._Unparser for the subset)
1522// ---------------------------------------------------------------------------
1523
1524/// `ast._Precedence`, verbatim — the derived `Ord` is the enum's
1525/// declaration order, matching the Python `IntEnum` values (`BOR` is an
1526/// alias of `EXPR` there; here `Expr` plays both roles).
1527#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1528#[allow(dead_code)] // full table kept verbatim; unsupported nodes never construct some levels
1529enum Prec {
1530    NamedExpr,
1531    Tuple,
1532    Yield,
1533    Test,
1534    Or,
1535    And,
1536    Not,
1537    Cmp,
1538    Expr,
1539    BXor,
1540    BAnd,
1541    Shift,
1542    Arith,
1543    Term,
1544    Factor,
1545    Power,
1546    Await,
1547    Atom,
1548}
1549
1550impl Prec {
1551    /// `_Precedence.next()` (saturating, like the Python `ValueError`
1552    /// fallback).
1553    fn next(self) -> Prec {
1554        match self {
1555            Prec::NamedExpr => Prec::Tuple,
1556            Prec::Tuple => Prec::Yield,
1557            Prec::Yield => Prec::Test,
1558            Prec::Test => Prec::Or,
1559            Prec::Or => Prec::And,
1560            Prec::And => Prec::Not,
1561            Prec::Not => Prec::Cmp,
1562            Prec::Cmp => Prec::Expr,
1563            Prec::Expr => Prec::BXor,
1564            Prec::BXor => Prec::BAnd,
1565            Prec::BAnd => Prec::Shift,
1566            Prec::Shift => Prec::Arith,
1567            Prec::Arith => Prec::Term,
1568            Prec::Term => Prec::Factor,
1569            Prec::Factor => Prec::Power,
1570            Prec::Power => Prec::Await,
1571            Prec::Await | Prec::Atom => Prec::Atom,
1572        }
1573    }
1574}
1575
1576impl PyOp {
1577    fn symbol(self) -> &'static str {
1578        match self {
1579            PyOp::Add => "+",
1580            PyOp::Sub => "-",
1581            PyOp::Mult => "*",
1582            PyOp::MatMult => "@",
1583            PyOp::Div => "/",
1584            PyOp::Mod => "%",
1585            PyOp::Pow => "**",
1586            PyOp::LShift => "<<",
1587            PyOp::RShift => ">>",
1588            PyOp::BitOr => "|",
1589            PyOp::BitXor => "^",
1590            PyOp::BitAnd => "&",
1591            PyOp::FloorDiv => "//",
1592        }
1593    }
1594
1595    /// `_Unparser.binop_precedence`.
1596    fn prec(self) -> Prec {
1597        match self {
1598            PyOp::Add | PyOp::Sub => Prec::Arith,
1599            PyOp::Mult | PyOp::MatMult | PyOp::Div | PyOp::Mod | PyOp::FloorDiv => Prec::Term,
1600            PyOp::LShift | PyOp::RShift => Prec::Shift,
1601            PyOp::BitOr => Prec::Expr,
1602            PyOp::BitXor => Prec::BXor,
1603            PyOp::BitAnd => Prec::BAnd,
1604            PyOp::Pow => Prec::Power,
1605        }
1606    }
1607}
1608
1609/// `_Unparser.items_view`: comma-separated, with a trailing comma when
1610/// there is exactly one item (tuple views).
1611fn items_view(out: &mut String, elts: &[PyExpr]) {
1612    if let [single] = elts {
1613        write_expr(out, single, Prec::Test);
1614        out.push(',');
1615    } else {
1616        write_joined(out, elts);
1617    }
1618}
1619
1620fn write_joined(out: &mut String, elts: &[PyExpr]) {
1621    for (i, e) in elts.iter().enumerate() {
1622        if i > 0 {
1623            out.push_str(", ");
1624        }
1625        write_expr(out, e, Prec::Test);
1626    }
1627}
1628
1629/// One node of `_Unparser.traverse`. `ctx` is the precedence the parent
1630/// `set_precedence`d onto this node (default `TEST`); a node whose own
1631/// precedence is lower gets parenthesized (`require_parens`).
1632fn write_expr(out: &mut String, e: &PyExpr, ctx: Prec) {
1633    match e {
1634        PyExpr::Name(n) => out.push_str(n),
1635        PyExpr::Constant(c) => write_const(out, c),
1636        PyExpr::Attribute(value, attr) => {
1637            write_expr(out, value, Prec::Atom);
1638            // "3.__abs__()" is a syntax error, so int constants get a
1639            // separating space: `(1).bit_length()` → `1 .bit_length()`.
1640            // bool is an int subclass in Python, so True/False qualify.
1641            if matches!(
1642                value.as_ref(),
1643                PyExpr::Constant(PyConst::Int(_) | PyConst::True | PyConst::False)
1644            ) {
1645                out.push(' ');
1646            }
1647            out.push('.');
1648            out.push_str(attr);
1649        }
1650        PyExpr::Subscript { value, slice } => {
1651            write_expr(out, value, Prec::Atom);
1652            out.push('[');
1653            match slice.as_ref() {
1654                // Parentheses can be omitted when the slice tuple isn't
1655                // empty; items_view keeps `x[1,]` and `x[*a,]` faithful.
1656                PyExpr::Tuple(elts) if !elts.is_empty() => items_view(out, elts),
1657                other => write_expr(out, other, Prec::Test),
1658            }
1659            out.push(']');
1660        }
1661        PyExpr::Call { func, args, kwargs } => {
1662            write_expr(out, func, Prec::Atom);
1663            out.push('(');
1664            let mut comma = false;
1665            for arg in args {
1666                if comma {
1667                    out.push_str(", ");
1668                }
1669                comma = true;
1670                write_expr(out, arg, Prec::Test);
1671            }
1672            for (name, value) in kwargs {
1673                if comma {
1674                    out.push_str(", ");
1675                }
1676                comma = true;
1677                out.push_str(name);
1678                out.push('=');
1679                write_expr(out, value, Prec::Test);
1680            }
1681            out.push(')');
1682        }
1683        PyExpr::Tuple(elts) => {
1684            let parens = elts.is_empty() || ctx > Prec::Tuple;
1685            if parens {
1686                out.push('(');
1687            }
1688            items_view(out, elts);
1689            if parens {
1690                out.push(')');
1691            }
1692        }
1693        PyExpr::List(elts) => {
1694            out.push('[');
1695            write_joined(out, elts);
1696            out.push(']');
1697        }
1698        PyExpr::Set(elts) => {
1699            if elts.is_empty() {
1700                // `{}` would be a dict and `set` might be shadowed —
1701                // _Unparser writes this (unreachable from the parser).
1702                out.push_str("{*()}");
1703            } else {
1704                out.push('{');
1705                write_joined(out, elts);
1706                out.push('}');
1707            }
1708        }
1709        PyExpr::Dict(items) => {
1710            out.push('{');
1711            for (i, (key, value)) in items.iter().enumerate() {
1712                if i > 0 {
1713                    out.push_str(", ");
1714                }
1715                match key {
1716                    Some(k) => {
1717                        write_expr(out, k, Prec::Test);
1718                        out.push_str(": ");
1719                        write_expr(out, value, Prec::Test);
1720                    }
1721                    None => {
1722                        out.push_str("**");
1723                        write_expr(out, value, Prec::Expr);
1724                    }
1725                }
1726            }
1727            out.push('}');
1728        }
1729        PyExpr::Starred(value) => {
1730            out.push('*');
1731            write_expr(out, value, Prec::Expr);
1732        }
1733        PyExpr::BoolOp { op, values } => {
1734            // `_Unparser.visit_BoolOp`: `operator_precedence` is bumped
1735            // once per value and the bump is CUMULATIVE (`nonlocal`), so
1736            // value 0 is rendered at `own.next()`, value 1 at
1737            // `own.next().next()`, and so on — which is why
1738            // `a or (b and c)` keeps its parentheses while
1739            // `a and b or c` does not.
1740            let (own, sep) = match op {
1741                PyBoolOp::Or => (Prec::Or, " or "),
1742                PyBoolOp::And => (Prec::And, " and "),
1743            };
1744            let parens = ctx > own;
1745            if parens {
1746                out.push('(');
1747            }
1748            let mut child = own;
1749            for (i, value) in values.iter().enumerate() {
1750                if i > 0 {
1751                    out.push_str(sep);
1752                }
1753                child = child.next();
1754                write_expr(out, value, child);
1755            }
1756            if parens {
1757                out.push(')');
1758            }
1759        }
1760        PyExpr::BinOp { left, op, right } => {
1761            let op_prec = op.prec();
1762            let parens = ctx > op_prec;
1763            if parens {
1764                out.push('(');
1765            }
1766            // `**` is the one right-associative operator: its left operand
1767            // needs the bumped precedence, everyone else bumps the right.
1768            let (left_prec, right_prec) = if matches!(op, PyOp::Pow) {
1769                (op_prec.next(), op_prec)
1770            } else {
1771                (op_prec, op_prec.next())
1772            };
1773            write_expr(out, left, left_prec);
1774            out.push(' ');
1775            out.push_str(op.symbol());
1776            out.push(' ');
1777            write_expr(out, right, right_prec);
1778            if parens {
1779                out.push(')');
1780            }
1781        }
1782        PyExpr::UnaryOp { op, operand } => {
1783            let (symbol, op_prec) = match op {
1784                PyUnaryOp::Invert => ("~", Prec::Factor),
1785                PyUnaryOp::Not => ("not", Prec::Not),
1786                PyUnaryOp::UAdd => ("+", Prec::Factor),
1787                PyUnaryOp::USub => ("-", Prec::Factor),
1788            };
1789            let parens = ctx > op_prec;
1790            if parens {
1791                out.push('(');
1792            }
1793            out.push_str(symbol);
1794            // Factor prefixes stick to their operand (`-1`, not `- 1`).
1795            if op_prec != Prec::Factor {
1796                out.push(' ');
1797            }
1798            write_expr(out, operand, op_prec);
1799            if parens {
1800                out.push(')');
1801            }
1802        }
1803    }
1804}
1805
1806fn write_const(out: &mut String, c: &PyConst) {
1807    match c {
1808        PyConst::None => out.push_str("None"),
1809        PyConst::True => out.push_str("True"),
1810        PyConst::False => out.push_str("False"),
1811        PyConst::Ellipsis => out.push_str("..."),
1812        PyConst::Int(digits) => out.push_str(digits),
1813        PyConst::Float(text) => out.push_str(text),
1814        PyConst::Str {
1815            value, u_prefix, ..
1816        } => {
1817            if *u_prefix {
1818                out.push('u');
1819            }
1820            // Recompute the quote from the value so hand-built constants
1821            // can't render inconsistently; the parser stores the same char.
1822            write_str_repr(out, value, repr_quote(value));
1823        }
1824        PyConst::Bytes(bytes) => write_bytes_repr(out, bytes),
1825    }
1826}
1827
1828// ---------------------------------------------------------------------------
1829// repr() for str and bytes
1830// ---------------------------------------------------------------------------
1831
1832/// CPython's quote selection: `'` unless the value contains `'` and no `"`.
1833fn repr_quote(value: &str) -> char {
1834    if value.contains('\'') && !value.contains('"') {
1835        '"'
1836    } else {
1837        '\''
1838    }
1839}
1840
1841/// Approximation of the non-`Cc` part of Python's `str.isprintable() ==
1842/// False` set (Zs/Zl/Zp except space, common Cf, Co). `char::is_control`
1843/// handles Cc separately; unassigned (Cn) code points are not covered —
1844/// the module docs carry that caveat.
1845fn is_nonprintable_nonascii(c: char) -> bool {
1846    matches!(u32::from(c),
1847        0xa0 | 0xad
1848        | 0x600..=0x605 | 0x61c | 0x6dd | 0x70f | 0x890..=0x891 | 0x8e2
1849        | 0x1680 | 0x180e
1850        | 0x2000..=0x200f | 0x2028..=0x202f | 0x205f..=0x2064 | 0x2066..=0x206f
1851        | 0x3000 | 0xfeff | 0xfff9..=0xfffb
1852        | 0xe000..=0xf8ff
1853        | 0x110bd | 0x110cd | 0x13430..=0x1343f | 0x1bca0..=0x1bca3
1854        | 0x1d173..=0x1d17a | 0xe0001 | 0xe0020..=0xe007f
1855        | 0xf0000..=0xffffd | 0x100000..=0x10fffd)
1856}
1857
1858/// CPython `unicode_repr`: escape backslash and the chosen quote; `\n`,
1859/// `\r`, `\t` mnemonically; other non-printables as `\xXX`/`\uXXXX`/
1860/// `\UXXXXXXXX` (lowercase hex).
1861fn write_str_repr(out: &mut String, value: &str, quote: char) {
1862    use fmt::Write as _;
1863    out.push(quote);
1864    for c in value.chars() {
1865        if c == quote || c == '\\' {
1866            out.push('\\');
1867            out.push(c);
1868        } else if c == '\n' {
1869            out.push_str("\\n");
1870        } else if c == '\r' {
1871            out.push_str("\\r");
1872        } else if c == '\t' {
1873            out.push_str("\\t");
1874        } else if c.is_control() || (!c.is_ascii() && is_nonprintable_nonascii(c)) {
1875            let u = u32::from(c);
1876            let _ = if u < 0x100 {
1877                write!(out, "\\x{u:02x}")
1878            } else if u < 0x10000 {
1879                write!(out, "\\u{u:04x}")
1880            } else {
1881                write!(out, "\\U{u:08x}")
1882            };
1883        } else {
1884            out.push(c);
1885        }
1886    }
1887    out.push(quote);
1888}
1889
1890/// CPython `bytes.__repr__` — fully deterministic ASCII output.
1891fn write_bytes_repr(out: &mut String, bytes: &[u8]) {
1892    use fmt::Write as _;
1893    let quote = if bytes.contains(&b'\'') && !bytes.contains(&b'"') {
1894        '"'
1895    } else {
1896        '\''
1897    };
1898    out.push('b');
1899    out.push(quote);
1900    for &b in bytes {
1901        if b == quote as u8 || b == b'\\' {
1902            out.push('\\');
1903            out.push(char::from(b));
1904        } else if b == b'\t' {
1905            out.push_str("\\t");
1906        } else if b == b'\n' {
1907            out.push_str("\\n");
1908        } else if b == b'\r' {
1909            out.push_str("\\r");
1910        } else if (0x20..0x7f).contains(&b) {
1911            out.push(char::from(b));
1912        } else {
1913            let _ = write!(out, "\\x{b:02x}");
1914        }
1915    }
1916    out.push(quote);
1917}
1918
1919#[cfg(test)]
1920mod tests {
1921    use super::{parse_py_expr, parse_py_expr_stmt, parse_py_star_annotation, unparse};
1922
1923    /// The oracle battery. Every `(source, expected)` pair below is
1924    /// probe-verified:
1925    // oracle: ast.unparse, python 3.12 / sphinx 9.1.0 / docutils 0.22.4 pin
1926    // (scratchpad oracle_expr.py; `uv run --python 3.12 --with
1927    // 'sphinx==9.1.0' --with 'docutils==0.22.4' python -c "import ast;
1928    // print(ast.unparse(ast.parse('<expr>', mode='eval')))"`).
1929    const ORACLE: &[(&str, &str)] = &[
1930        // brief-mandated normalization pins
1931        ("1+2", "1 + 2"),
1932        ("dict[str,int]", "dict[str, int]"),
1933        ("\"x\"", "'x'"),
1934        ("[1 ,2]", "[1, 2]"),
1935        ("( 1, )", "(1,)"),
1936        ("{'a':1}", "{'a': 1}"),
1937        ("-1", "-1"),
1938        ("x [ 1 ]", "x[1]"),
1939        // nesting / typing shapes
1940        ("dict[str, list[int]]", "dict[str, list[int]]"),
1941        ("tuple[int, ...]", "tuple[int, ...]"),
1942        ("Callable[[int], str]", "Callable[[int], str]"),
1943        ("int | None", "int | None"),
1944        ("Optional[int] | str", "Optional[int] | str"),
1945        ("a.b.c.d", "a.b.c.d"),
1946        ("a . b . c", "a.b.c"),
1947        // integers
1948        ("0xFF", "255"),
1949        ("1_000", "1000"),
1950        ("0o777", "511"),
1951        ("0b1010", "10"),
1952        ("0x_FF", "255"),
1953        ("000", "0"),
1954        ("0_0", "0"),
1955        ("0x0", "0"),
1956        ("999999999999999999999999", "999999999999999999999999"),
1957        ("0xFFFFFFFFFFFFFFFFFFFF", "1208925819614629174706175"),
1958        // floats (repr(float) semantics)
1959        ("1e-3", "0.001"),
1960        ("1E3", "1000.0"),
1961        ("1e+3", "1000.0"),
1962        (".5", "0.5"),
1963        ("5.", "5.0"),
1964        ("5.0", "5.0"),
1965        ("0.1", "0.1"),
1966        ("2.675", "2.675"),
1967        ("10_000_000.0", "10000000.0"),
1968        ("1e15", "1000000000000000.0"),
1969        ("1e16", "1e+16"),
1970        ("1e300", "1e+300"),
1971        ("1e-4", "0.0001"),
1972        ("0.0001", "0.0001"),
1973        ("0.00001", "1e-05"),
1974        ("1e-5", "1e-05"),
1975        ("1.5e10", "15000000000.0"),
1976        ("1e400", "1e309"),
1977        ("0e0", "0.0"),
1978        ("00.0", "0.0"),
1979        ("9007199254740993.0", "9007199254740992.0"),
1980        ("123456789123456789.0", "1.2345678912345678e+17"),
1981        ("1.7976931348623157e308", "1.7976931348623157e+308"),
1982        ("5e-324", "5e-324"),
1983        ("-1.0", "-1.0"),
1984        ("-0.0", "-0.0"),
1985        // strings and bytes
1986        ("\"a'b\"", "\"a'b\""),
1987        ("'a\"b'", "'a\"b'"),
1988        ("'both\\'\\\"'", "'both\\'\"'"),
1989        ("'don\\'t \"quote\"'", "'don\\'t \"quote\"'"),
1990        ("'ab' 'cd'", "'abcd'"),
1991        ("'x' 'y' 'z'", "'xyz'"),
1992        ("b'x'", "b'x'"),
1993        ("B\"y\"", "b'y'"),
1994        ("u'x'", "u'x'"),
1995        ("u''", "u''"),
1996        ("b''", "b''"),
1997        ("''", "''"),
1998        ("u'a' 'b'", "u'ab'"),
1999        ("'a' u'b'", "'ab'"),
2000        ("b'a' b'b'", "b'ab'"),
2001        ("r'\\d'", "'\\\\d'"),
2002        ("rb'\\x00'", "b'\\\\x00'"),
2003        ("'\\n'", "'\\n'"),
2004        ("'\\x41'", "'A'"),
2005        ("'\\t\\\\'", "'\\t\\\\'"),
2006        ("'\\''", "\"'\""),
2007        ("\"\\\"\"", "'\"'"),
2008        ("'\\u00e9'", "'é'"),
2009        ("'é'", "'é'"),
2010        ("'\\x85'", "'\\x85'"),
2011        ("'\\xa0'", "'\\xa0'"),
2012        ("'\\u2028'", "'\\u2028'"),
2013        ("'\u{202f}'", "'\\u202f'"),
2014        ("'\\x7f'", "'\\x7f'"),
2015        ("'\\x00'", "'\\x00'"),
2016        ("'\\v'", "'\\x0b'"),
2017        ("'\\a'", "'\\x07'"),
2018        ("'\\401'", "'ā'"),
2019        ("'\\x9F'", "'\\x9f'"),
2020        ("b'\\x00\\xff'", "b'\\x00\\xff'"),
2021        ("b'a\\'b'", "b\"a'b\""),
2022        ("b'\\''", "b\"'\""),
2023        ("b'\"'", "b'\"'"),
2024        ("b'both\\'\\\"'", "b'both\\'\"'"),
2025        ("b'\\401'", "b'\\x01'"),
2026        // triple-quoted literals
2027        ("'''x'''", "'x'"),
2028        ("\"\"\"a'b\"\"\"", "\"a'b\""),
2029        ("'''a\nb'''", "'a\\nb'"),
2030        ("'''it's'''", "\"it's\""),
2031        ("u'''k'''", "u'k'"),
2032        ("b'''q'''", "b'q'"),
2033        ("'''trip''' 'le'", "'triple'"),
2034        // containers
2035        ("[]", "[]"),
2036        ("{}", "{}"),
2037        ("()", "()"),
2038        ("(1,)", "(1,)"),
2039        ("(1, 2)", "(1, 2)"),
2040        ("1, 2", "(1, 2)"),
2041        ("1,", "(1,)"),
2042        ("{1, 2}", "{1, 2}"),
2043        ("{'a': 1, 'b': 2}", "{'a': 1, 'b': 2}"),
2044        ("{**base, 'a': 1}", "{**base, 'a': 1}"),
2045        ("{**a}", "{**a}"),
2046        ("{**a | b}", "{**a | b}"),
2047        ("{1: (2, 3)}", "{1: (2, 3)}"),
2048        ("[*a, 1]", "[*a, 1]"),
2049        ("(*a, 1)", "(*a, 1)"),
2050        ("{*a, 1}", "{*a, 1}"),
2051        ("[[1, 2], [3]]", "[[1, 2], [3]]"),
2052        ("[(1, 2)]", "[(1, 2)]"),
2053        ("((1, 2),)", "((1, 2),)"),
2054        // subscripts
2055        ("x[1,]", "x[1,]"),
2056        ("x[()]", "x[()]"),
2057        ("x[1, 2]", "x[1, 2]"),
2058        ("x[(1, 2)]", "x[1, 2]"),
2059        ("x[*a]", "x[*a,]"),
2060        ("x[a, *b]", "x[a, *b]"),
2061        ("x[a, *b, c]", "x[a, *b, c]"),
2062        ("x[a][b]", "x[a][b]"),
2063        ("x[-1]", "x[-1]"),
2064        ("x[...]", "x[...]"),
2065        // calls
2066        ("f()", "f()"),
2067        ("f(1, x=2)", "f(1, x=2)"),
2068        ("f(*args)", "f(*args)"),
2069        ("f(x=1)", "f(x=1)"),
2070        ("f(1, *a, x=2)", "f(1, *a, x=2)"),
2071        ("f(1, x=2, *a, y=3)", "f(1, *a, x=2, y=3)"),
2072        ("f(a)(b)[c].d", "f(a)(b)[c].d"),
2073        ("f(a, *b)(c)", "f(a, *b)(c)"),
2074        ("(a + b).method(x)", "(a + b).method(x)"),
2075        ("(1).bit_length()", "1 .bit_length()"),
2076        ("f(g(x), y=h(z))", "f(g(x), y=h(z))"),
2077        ("f((1, 2))", "f((1, 2))"),
2078        ("f(-1)", "f(-1)"),
2079        // constants
2080        ("...", "..."),
2081        ("True", "True"),
2082        ("False", "False"),
2083        ("None", "None"),
2084        ("True | False", "True | False"),
2085        // operators / precedence (ports of ast._Unparser paren rules)
2086        ("2**8", "2 ** 8"),
2087        ("2 ** -1", "2 ** (-1)"),
2088        ("-2 ** 2", "-2 ** 2"),
2089        ("(-2) ** 2", "(-2) ** 2"),
2090        ("-(2 ** 2)", "-2 ** 2"),
2091        ("-x ** 2", "-x ** 2"),
2092        ("2 ** -x ** 3", "2 ** (-x ** 3)"),
2093        ("a ** b ** c", "a ** b ** c"),
2094        ("(a ** b) ** c", "(a ** b) ** c"),
2095        ("(1 + 2) * 3", "(1 + 2) * 3"),
2096        ("1 + (2 * 3)", "1 + 2 * 3"),
2097        ("1 + 2 * 3", "1 + 2 * 3"),
2098        ("a | b | c", "a | b | c"),
2099        ("a | (b | c)", "a | (b | c)"),
2100        ("a + b | c & d", "a + b | c & d"),
2101        ("a << 2 >> 1", "a << 2 >> 1"),
2102        ("x & y ^ z", "x & y ^ z"),
2103        ("x @ y", "x @ y"),
2104        ("(a @ b) @ c", "a @ b @ c"),
2105        ("a @ (b @ c)", "a @ (b @ c)"),
2106        ("x // y % z", "x // y % z"),
2107        ("a / b * c", "a / b * c"),
2108        ("a - (b - c)", "a - (b - c)"),
2109        ("(a - b) - c", "a - b - c"),
2110        ("a % (b % c)", "a % (b % c)"),
2111        ("~x", "~x"),
2112        ("+x", "+x"),
2113        ("not x", "not x"),
2114        ("not not x", "not not x"),
2115        ("- -x", "--x"),
2116        ("-(-x)", "--x"),
2117        ("-(1)", "-1"),
2118        ("-(a + b)", "-(a + b)"),
2119        ("~x | +y", "~x | +y"),
2120        ("not a | b", "not a | b"),
2121        ("(a | b)[c]", "(a | b)[c]"),
2122        ("-x[0]", "-x[0]"),
2123        ("(-x)[0]", "(-x)[0]"),
2124        // `ast.BoolOp`. `_Unparser.visit_BoolOp` bumps its precedence
2125        // CUMULATIVELY across the values, so the parenthesization is
2126        // asymmetric: `a and b or c` needs none, `a or b and c` does.
2127        ("a or b", "a or b"),
2128        ("a and b or c", "a and b or c"),
2129        ("a or b and c", "a or (b and c)"),
2130        ("(a or b) and c", "(a or b) and c"),
2131        ("not a or b", "not a or b"),
2132        ("a or not b", "a or not b"),
2133        ("a | b or c", "a | b or c"),
2134        ("a or b or c", "a or b or c"),
2135        ("a and b and c", "a and b and c"),
2136        ("(a or b) or c", "(a or b) or c"),
2137        ("a or (b or c)", "a or (b or c)"),
2138        ("-a or b", "-a or b"),
2139        ("f(a or b)", "f(a or b)"),
2140        ("[a or b]", "[a or b]"),
2141        ("a or b, c", "(a or b, c)"),
2142        ("x[a or b]", "x[a or b]"),
2143        ("(a or b).c", "(a or b).c"),
2144        ("(a and b)(c)", "(a and b)(c)"),
2145        ("not (a or b)", "not (a or b)"),
2146        ("a or b ** c", "a or b ** c"),
2147        ("(a or b)[0]", "(a or b)[0]"),
2148        ("{a or b}", "{a or b}"),
2149        ("{(a or b): 1}", "{a or b: 1}"),
2150        ("{'k': a or b}", "{'k': a or b}"),
2151        ("a or b | c", "a or b | c"),
2152        ("(a and b) or (c and d)", "a and b or (c and d)"),
2153        ("a and (b or c)", "a and (b or c)"),
2154        ("a or b or c or d", "a or b or c or d"),
2155        ("a and b or c and d", "a and b or (c and d)"),
2156        ("f(*(a or b))", "f(*(a or b))"),
2157        ("x[a or b, c]", "x[a or b, c]"),
2158        ("a  or   b", "a or b"),
2159        ("not not a or b", "not not a or b"),
2160        ("True or False", "True or False"),
2161        ("a or 1 or 'x'", "a or 1 or 'x'"),
2162    ];
2163
2164    /// Inputs that must be `Err` — never a panic. Some are invalid Python;
2165    /// the rest are valid Python outside the supported subset (the brief's
2166    /// explicit exclusions), where `Err` routes callers into Sphinx's
2167    /// fallback paths.
2168    const ERR_CASES: &[&str] = &[
2169        // brief-pinned exclusions
2170        "lambda: 1",
2171        "x if y else z",
2172        "(x := 1)",
2173        "",
2174        "f(a",
2175        "1 +",
2176        "[1,, 2]",
2177        "await x",
2178        "f'{x}'",
2179        "x for x in y",
2180        "(x for x in y)",
2181        "[x for x in y]",
2182        // outside the enum's subset (valid Python, deliberate Err)
2183        "x < y",
2184        "x == y",
2185        "x[1:2]",
2186        "x[:]",
2187        "f(**kw)",
2188        "1j",
2189        "'\\N{DASH}'",
2190        "yield x",
2191        "1 if True else 2",
2192        "1if True else 2",
2193        // plain syntax errors (Python errs too)
2194        "*x",
2195        "(*x)",
2196        "*a, b",
2197        "b, *a",
2198        "*a,",
2199        "x = 1",
2200        "01",
2201        "09",
2202        "1_",
2203        "1__0",
2204        "0x",
2205        "0b2",
2206        "def f(): pass",
2207        "x²",
2208        "'unterminated",
2209        "b'unterminated",
2210        "f(x=1, 2)",
2211        "1 2",
2212        "x[]",
2213        "{*a: 1}",
2214        "'a' b'b'",
2215        "b'é'",
2216        "x.if",
2217        "f(if=1)",
2218        "x + not y",
2219        ")",
2220        "((((",
2221        "\\",
2222        "..",
2223        "@x",
2224        "x;",
2225        "x, = y",
2226        "'\\ud800'",
2227    ];
2228
2229    #[test]
2230    fn oracle_battery_round_trips_ast_unparse() {
2231        for (src, want) in ORACLE {
2232            let parsed = parse_py_expr(src)
2233                .unwrap_or_else(|e| panic!("parse_py_expr({src:?}) unexpectedly failed: {e}"));
2234            let got = unparse(&parsed);
2235            assert_eq!(&got, want, "unparse mismatch for source {src:?}");
2236        }
2237    }
2238
2239    /// `unparse` output must itself reparse to the same rendering —
2240    /// `ast.unparse(ast.parse(ast.unparse(t)))` is a fixed point.
2241    #[test]
2242    fn unparse_is_idempotent_over_reparse() {
2243        for (src, _) in ORACLE {
2244            let first = unparse(&parse_py_expr(src).unwrap());
2245            let reparsed = parse_py_expr(&first)
2246                .unwrap_or_else(|e| panic!("unparse output {first:?} must reparse: {e}"));
2247            assert_eq!(unparse(&reparsed), first, "not idempotent for {src:?}");
2248        }
2249    }
2250
2251    /// Exec mode is what `_parse_annotation` uses, so a top-level starred
2252    /// expression is a legal `Expr` statement there and a blank source is
2253    /// `Module(body=[])`.
2254    ///
2255    // oracle (pinned toolchain, scratchpad A/p1.py + A/p3.py):
2256    //   ast.parse('*Ts')       -> Module(body=[Expr(Starred(Name('Ts')))])
2257    //   ast.parse('*a, b')     -> Module(body=[Expr(Tuple([Starred(a), b]))])
2258    //   ast.parse('')/' '/'\n' -> Module(body=[])
2259    //   ast.parse('  int')     -> IndentationError: unexpected indent
2260    //   ast.parse('\x0cint')   -> Module(body=[Expr(Name('int'))])
2261    #[test]
2262    fn exec_mode_accepts_top_level_starred_and_empty_sources() {
2263        for (src, want) in [
2264            ("*Ts", "*Ts"),
2265            ("* Ts", "*Ts"),
2266            ("*tuple[int, ...]", "*tuple[int, ...]"),
2267            ("*a.b", "*a.b"),
2268            ("*a | b", "*a | b"),
2269            ("*-a", "*-a"),
2270            ("*a, b", "(*a, b)"),
2271            ("a, *b", "(a, *b)"),
2272            ("*a,", "(*a,)"),
2273            ("int", "int"),
2274            ("\nint", "int"),
2275            ("  \nint", "int"),
2276            ("\u{c}int", "int"),
2277            ("int ", "int"),
2278        ] {
2279            let parsed = parse_py_expr_stmt(src)
2280                .unwrap_or_else(|e| panic!("parse_py_expr_stmt({src:?}) failed: {e}"))
2281                .unwrap_or_else(|| panic!("parse_py_expr_stmt({src:?}) yielded no statement"));
2282            assert_eq!(unparse(&parsed), want, "exec-mode unparse for {src:?}");
2283        }
2284        for src in ["", " ", "  ", "\n", "\u{c}", "\t\n \n"] {
2285            assert_eq!(
2286                parse_py_expr_stmt(src),
2287                Ok(None),
2288                "{src:?} must be Module(body=[])"
2289            );
2290        }
2291        // IndentationError is a SyntaxError subclass, so the caller keeps
2292        // the unstripped text — never a silently stripped parse.
2293        for src in ["  int", " int", "\tint", " \u{c} int", "int\n  str"] {
2294            assert!(
2295                parse_py_expr_stmt(src).is_err(),
2296                "{src:?} must raise IndentationError"
2297            );
2298        }
2299        // Eval mode still rejects every top-level star (ledger ruling).
2300        for src in ["*Ts", "*a, b", "*a,", "a, *b"] {
2301            assert!(parse_py_expr(src).is_err(), "eval mode must reject {src:?}");
2302        }
2303    }
2304
2305    /// CPython's `star_annotation` production: one leading `*`, never a
2306    /// starred tuple element.
2307    ///
2308    // oracle: ast.parse('def f(*args: *Ts): pass') is legal;
2309    // 'def f(x: *Ts)' and 'def f(**k: *Ts)' are SyntaxErrors, and
2310    // signature_from_str('(*args: *tuple[int, ...])') gives the annotation
2311    // string '*tuple[int, ...]' (scratchpad A/p2.py).
2312    #[test]
2313    fn star_annotation_mode_takes_one_leading_star_only() {
2314        for (src, want) in [
2315            ("*Ts", "*Ts"),
2316            ("*tuple[int, ...]", "*tuple[int, ...]"),
2317            ("int", "int"),
2318            ("int | None", "int | None"),
2319        ] {
2320            let parsed = parse_py_star_annotation(src)
2321                .unwrap_or_else(|e| panic!("parse_py_star_annotation({src:?}) failed: {e}"));
2322            assert_eq!(unparse(&parsed), want);
2323        }
2324        for src in ["*a, b", "a, *b", "*a,", ""] {
2325            assert!(
2326                parse_py_star_annotation(src).is_err(),
2327                "star_annotation must reject {src:?}"
2328            );
2329        }
2330    }
2331
2332    #[test]
2333    fn unsupported_and_invalid_inputs_are_errors_not_panics() {
2334        for src in ERR_CASES {
2335            assert!(
2336                parse_py_expr(src).is_err(),
2337                "expected Err for source {src:?}"
2338            );
2339        }
2340    }
2341
2342    /// Pathologically nested input must hit the depth limit (Err), not
2343    /// overflow the stack. The right-extending shapes stress the recursion
2344    /// guards; the left-extending shapes (trailer loops, binop folds)
2345    /// stress the `charge_node` budget — without it they would return an
2346    /// `Ok` tree whose recursive `unparse`/`Drop` aborts the process.
2347    #[test]
2348    fn deep_nesting_is_an_error_not_a_stack_overflow() {
2349        for (open, close) in [("(", ")"), ("[", "]"), ("-", "")] {
2350            let src = format!("{}1{}", open.repeat(5000), close.repeat(5000));
2351            assert!(parse_py_expr(&src).is_err(), "expected Err for deep {open}");
2352        }
2353        for src in [
2354            format!("a{}", ".b".repeat(5000)),
2355            format!("x{}", "[1]".repeat(5000)),
2356            format!("f{}", "()".repeat(5000)),
2357            format!("1{}", "+1".repeat(5000)),
2358        ] {
2359            assert!(
2360                parse_py_expr(&src).is_err(),
2361                "expected Err for left-deep input starting {:?}",
2362                &src[..8]
2363            );
2364        }
2365    }
2366
2367    /// The stored `quote` on a str constant agrees with what `unparse`
2368    /// renders (repr's quote-selection rule).
2369    #[test]
2370    fn stored_quote_matches_rendered_quote() {
2371        for (src, quote) in [("'x'", '\''), ("\"a'b\"", '"'), ("'a\"b'", '\'')] {
2372            let parsed = parse_py_expr(src).unwrap();
2373            match parsed {
2374                super::PyExpr::Constant(super::PyConst::Str { quote: q, .. }) => {
2375                    assert_eq!(q, quote, "quote for {src:?}");
2376                }
2377                other => panic!("expected Str constant for {src:?}, got {other:?}"),
2378            }
2379        }
2380    }
2381
2382    mod proptests {
2383        use super::super::parse_py_expr;
2384        use proptest::prelude::*;
2385
2386        proptest! {
2387            #![proptest_config(ProptestConfig { cases: 512, ..ProptestConfig::default() })]
2388
2389            /// Totality: arbitrary input never panics (T16 extends this).
2390            #[test]
2391            fn parse_never_panics_on_arbitrary_input(s in "\\PC*") {
2392                let _ = parse_py_expr(&s);
2393            }
2394
2395            /// Expression-shaped fragments never panic, and successful
2396            /// parses unparse to a reparse fixed point.
2397            #[test]
2398            fn parse_never_panics_on_expr_shaped_input(
2399                s in proptest::collection::vec(
2400                    prop_oneof![
2401                        Just("x".to_string()),
2402                        Just("1".to_string()),
2403                        Just("1.5".to_string()),
2404                        Just("'s'".to_string()),
2405                        Just("b'q'".to_string()),
2406                        Just("(".to_string()),
2407                        Just(")".to_string()),
2408                        Just("[".to_string()),
2409                        Just("]".to_string()),
2410                        Just("{".to_string()),
2411                        Just("}".to_string()),
2412                        Just(",".to_string()),
2413                        Just(":".to_string()),
2414                        Just(".".to_string()),
2415                        Just("...".to_string()),
2416                        Just("**".to_string()),
2417                        Just("*".to_string()),
2418                        Just("|".to_string()),
2419                        Just("-".to_string()),
2420                        Just("=".to_string()),
2421                        Just("not ".to_string()),
2422                        Just(" ".to_string()),
2423                    ],
2424                    0..24,
2425                ).prop_map(|v| v.concat())
2426            ) {
2427                if let Ok(parsed) = parse_py_expr(&s) {
2428                    let out = super::super::unparse(&parsed);
2429                    let reparsed = parse_py_expr(&out).expect("unparse output reparses");
2430                    prop_assert_eq!(super::super::unparse(&reparsed), out);
2431                }
2432            }
2433        }
2434    }
2435}