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#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
13pub struct JoinedStr {
14 pub values: Vec<ExprType>,
16 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#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
25pub struct FormattedValue {
26 pub value: Box<ExprType>,
28 pub conversion: Option<i32>,
30 pub format_spec: Option<Box<ExprType>>,
32 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 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 let value: ExprType = ob.getattr("value")?.extract()?;
60
61 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 } else {
67 Some(conv_val)
68 }
69 } else {
70 None
71 };
72
73 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 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
160fn 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
170fn escape_format_braces(s: &str) -> String {
172 s.replace('{', "{{").replace('}', "}}")
173}
174
175impl FormattedValue {
176 fn rust_placeholder(
184 &self,
185 value: TokenStream,
186 ) -> Result<(String, TokenStream), Box<dyn std::error::Error>> {
187 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 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 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
262fn 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 }