Skip to main content

python_ast/ast/tree/
f_string.rs

1use proc_macro2::TokenStream;
2use pyo3::{Borrowed, FromPyObject, PyAny, PyResult, prelude::PyAnyMethods};
3use quote::quote;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    CodeGen, CodeGenContext, ExprType, Node, PythonOptions, SymbolTableScopes,
8    extract_list,
9};
10
11/// Joined string (f-string, e.g., f"Hello {name}")
12#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
13pub struct JoinedStr {
14    /// The values that make up the f-string (mix of strings and expressions)
15    pub values: Vec<ExprType>,
16    /// Position information
17    pub lineno: Option<usize>,
18    pub col_offset: Option<usize>,
19    pub end_lineno: Option<usize>,
20    pub end_col_offset: Option<usize>,
21}
22
23/// Formatted value within an f-string (e.g., the {name} part)
24#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
25pub struct FormattedValue {
26    /// The expression to be formatted
27    pub value: Box<ExprType>,
28    /// Conversion flag (None, 's', 'r', 'a') - represented as optional integer
29    pub conversion: Option<i32>,
30    /// Format specifier (optional)
31    pub format_spec: Option<Box<ExprType>>,
32    /// Position information
33    pub lineno: Option<usize>,
34    pub col_offset: Option<usize>,
35    pub end_lineno: Option<usize>,
36    pub end_col_offset: Option<usize>,
37}
38
39impl<'a, 'py> FromPyObject<'a, 'py> for JoinedStr {
40    type Error = pyo3::PyErr;
41    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
42        // Extract values
43        let values: Vec<ExprType> = extract_list(&ob, "values", "joined string values")?;
44        
45        Ok(JoinedStr {
46            values,
47            lineno: ob.lineno(),
48            col_offset: ob.col_offset(),
49            end_lineno: ob.end_lineno(),
50            end_col_offset: ob.end_col_offset(),
51        })
52    }
53}
54
55impl<'a, 'py> FromPyObject<'a, 'py> for FormattedValue {
56    type Error = pyo3::PyErr;
57    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
58        // Extract value
59        let value: ExprType = ob.getattr("value")?.extract()?;
60        
61        // Extract conversion (optional)
62        let conversion: Option<i32> = if let Ok(conv_attr) = ob.getattr("conversion") {
63            let conv_val: i32 = conv_attr.extract()?;
64            if conv_val == -1 {
65                None // -1 means no conversion
66            } else {
67                Some(conv_val)
68            }
69        } else {
70            None
71        };
72        
73        // Extract format_spec (optional)
74        let format_spec: Option<Box<ExprType>> = if let Ok(spec_attr) = ob.getattr("format_spec") {
75            if spec_attr.is_none() {
76                None
77            } else {
78                Some(Box::new(spec_attr.extract()?))
79            }
80        } else {
81            None
82        };
83        
84        Ok(FormattedValue {
85            value: Box::new(value),
86            conversion,
87            format_spec,
88            lineno: ob.lineno(),
89            col_offset: ob.col_offset(),
90            end_lineno: ob.end_lineno(),
91            end_col_offset: ob.end_col_offset(),
92        })
93    }
94}
95
96impl Node for JoinedStr {
97    fn lineno(&self) -> Option<usize> { self.lineno }
98    fn col_offset(&self) -> Option<usize> { self.col_offset }
99    fn end_lineno(&self) -> Option<usize> { self.end_lineno }
100    fn end_col_offset(&self) -> Option<usize> { self.end_col_offset }
101}
102
103impl Node for FormattedValue {
104    fn lineno(&self) -> Option<usize> { self.lineno }
105    fn col_offset(&self) -> Option<usize> { self.col_offset }
106    fn end_lineno(&self) -> Option<usize> { self.end_lineno }
107    fn end_col_offset(&self) -> Option<usize> { self.end_col_offset }
108}
109
110impl CodeGen for JoinedStr {
111    type Context = CodeGenContext;
112    type Options = PythonOptions;
113    type SymbolTable = SymbolTableScopes;
114
115    fn find_symbols(self, symbols: Self::SymbolTable) -> Self::SymbolTable {
116        self.values.into_iter().fold(symbols, |acc, val| val.find_symbols(acc))
117    }
118
119    fn to_rust(
120        self,
121        ctx: Self::Context,
122        options: Self::Options,
123        symbols: Self::SymbolTable,
124    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
125        // Build a single format! call: literal parts go into the format
126        // string (with `{`/`}` escaped), interpolated parts each get a
127        // placeholder and become an argument.
128        let mut fmt = String::new();
129        let mut args: Vec<TokenStream> = Vec::new();
130
131        for val in self.values {
132            match val {
133                ExprType::Constant(c) => {
134                    fmt.push_str(&escape_format_braces(&constant_text(&c)));
135                }
136                ExprType::FormattedValue(fv) => {
137                    let expr = (*fv.value)
138                        .clone()
139                        .to_rust(ctx.clone(), options.clone(), symbols.clone())?;
140                    let (placeholder, expr) = fv.rust_placeholder(expr)?;
141                    fmt.push_str(&placeholder);
142                    args.push(expr);
143                }
144                other => {
145                    let expr = other.to_rust(ctx.clone(), options.clone(), symbols.clone())?;
146                    fmt.push_str("{}");
147                    args.push(quote!(py_display(&(#expr))));
148                }
149            }
150        }
151
152        if fmt.is_empty() && args.is_empty() {
153            Ok(quote! { String::new() })
154        } else {
155            Ok(quote! { format!(#fmt #(, #args)*) })
156        }
157    }
158}
159
160/// Recover the unescaped text of a string constant (its stored form is a
161/// quoted, escaped Rust literal).
162fn constant_text(c: &crate::Constant) -> String {
163    match &c.0 {
164        Some(litrs::Literal::String(s)) => s.value().to_string(),
165        Some(other) => other.to_string(),
166        None => String::new(),
167    }
168}
169
170/// Escape literal braces so they survive inside a format! string.
171fn escape_format_braces(s: &str) -> String {
172    s.replace('{', "{{").replace('}', "}}")
173}
174
175impl FormattedValue {
176    /// The Rust format placeholder for this interpolation, plus a wrapper
177    /// applied to the value expression when the spec demands it (f64
178    /// coercion for `f` types, the runtime radix formatter for x/X/o/b —
179    /// Rust would print negative values as two's complement). `!r`/`!a`
180    /// conversions map to `{:?}`. Specs Rust cannot reproduce exactly (or
181    /// that interpolate other values) are LOUD conversion errors — falling
182    /// back to `{}` would silently change the output.
183    fn rust_placeholder(
184        &self,
185        value: TokenStream,
186    ) -> Result<(String, TokenStream), Box<dyn std::error::Error>> {
187        // Python conversion codes are the ASCII values of 's', 'r', 'a'.
188        let is_repr = matches!(self.conversion, Some(114) | Some(97));
189
190        let spec_text = match &self.format_spec {
191            None => String::new(),
192            Some(spec) => match static_spec_text(spec) {
193                None => {
194                    return Err(
195                        "f-string format specs that interpolate other values (e.g. \
196                         f\"{x:{width}}\") are not supported yet"
197                            .to_string()
198                            .into(),
199                    );
200                }
201                Some(text) => text.trim().to_string(),
202            },
203        };
204
205        use crate::pyformat::SpecLowering;
206        // `!r` renders the repr STRING first and lets the spec pad or
207        // align that string, exactly as Python does — Rust's `{:?}`
208        // would print its own Debug form ("abc" with double quotes,
209        // `true` for a bool) and silently diverge.
210        if is_repr {
211            let lowering = crate::pyformat::translate_format_spec(&spec_text)
212                .map_err(|e| format!("f-string: {}", e))?;
213            let SpecLowering::Inline(suffix) = lowering else {
214                return Err("numeric presentation types cannot combine with !r/!a (Python \
215                            applies the spec to the repr string and raises)"
216                    .to_string()
217                    .into());
218            };
219            let placeholder = if suffix.is_empty() {
220                "{}".to_string()
221            } else {
222                format!("{{:{}}}", suffix)
223            };
224            return Ok((placeholder, quote!(repr(&(#value)))));
225        }
226        let lowering = crate::pyformat::translate_format_spec(&spec_text)
227            .map_err(|e| format!("f-string: {}", e))?;
228        match lowering {
229            // A bare `{x}` is Python's str(), which is NOT Rust's
230            // Display: Display prints `1` for 1.0 and `true` for True.
231            SpecLowering::Inline(suffix) if suffix.is_empty() => {
232                Ok(("{}".to_string(), quote!(py_display(&(#value)))))
233            }
234            SpecLowering::Inline(suffix) => Ok((format!("{{:{}}}", suffix), value)),
235            SpecLowering::CastF64(suffix) => Ok((
236                if suffix.is_empty() {
237                    "{}".to_string()
238                } else {
239                    format!("{{:{}}}", suffix)
240                },
241                quote!(((#value) as f64)),
242            )),
243            SpecLowering::IntRadix {
244                fill,
245                align,
246                plus,
247                alternate,
248                zero,
249                width,
250                radix,
251            } => Ok((
252                "{}".to_string(),
253                quote!(py_int_radix_format(
254                    #value, #fill, #align, #plus, #alternate, #zero, #width,
255                    #radix,
256                )),
257            )),
258        }
259    }
260}
261
262/// If a format spec is a purely constant expression, return its text.
263fn static_spec_text(spec: &ExprType) -> Option<String> {
264    match spec {
265        ExprType::Constant(c) => Some(constant_text(c)),
266        ExprType::JoinedStr(js) => {
267            let mut out = String::new();
268            for part in &js.values {
269                if let ExprType::Constant(c) = part {
270                    out.push_str(&constant_text(c));
271                } else {
272                    return None;
273                }
274            }
275            Some(out)
276        }
277        _ => None,
278    }
279}
280
281impl CodeGen for FormattedValue {
282    type Context = CodeGenContext;
283    type Options = PythonOptions;
284    type SymbolTable = SymbolTableScopes;
285
286    fn find_symbols(self, symbols: Self::SymbolTable) -> Self::SymbolTable {
287        let symbols = (*self.value).find_symbols(symbols);
288        if let Some(format_spec) = self.format_spec {
289            (*format_spec).find_symbols(symbols)
290        } else {
291            symbols
292        }
293    }
294
295    fn to_rust(
296        self,
297        ctx: Self::Context,
298        options: Self::Options,
299        symbols: Self::SymbolTable,
300    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
301        let value_tokens = (*self.value).clone().to_rust(ctx, options, symbols)?;
302        let (placeholder, value_tokens) = self.rust_placeholder(value_tokens)?;
303        Ok(quote! {
304            format!(#placeholder, #value_tokens)
305        })
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    // Tests would go here - currently commented out as they need full AST infrastructure
312    // create_parse_test!(test_simple_fstring, "f'Hello {name}'", "test.py");
313}