Skip to main content

ryo_source/pure/to_syn/
expr.rs

1//! ToSyn implementations for expressions, patterns, and match arms.
2
3use proc_macro2::Span;
4use syn::token;
5
6use super::helpers::{ident, try_parse_path};
7use super::{ToSyn, ToSynError};
8use crate::pure::ast::{MacroDelimiter, PureExpr, PureMatchArm, PurePattern};
9
10/// Create a syn::Label from a label name (e.g., "outer" -> 'outer:)
11fn make_label(name: &str) -> syn::Label {
12    syn::Label {
13        name: syn::Lifetime {
14            apostrophe: Span::call_site(),
15            ident: proc_macro2::Ident::new(name, Span::call_site()),
16        },
17        colon_token: token::Colon::default(),
18    }
19}
20
21/// Create a syn::Lifetime from a label name (e.g., "outer" -> 'outer)
22fn make_lifetime(name: &str) -> syn::Lifetime {
23    syn::Lifetime {
24        apostrophe: Span::call_site(),
25        ident: proc_macro2::Ident::new(name, Span::call_site()),
26    }
27}
28
29impl ToSyn for PurePattern {
30    type Output = syn::Pat;
31
32    fn to_syn(&self) -> Result<syn::Pat, ToSynError> {
33        match self {
34            PurePattern::Ident {
35                name,
36                is_mut,
37                by_ref,
38            } => Ok(syn::Pat::Ident(syn::PatIdent {
39                attrs: vec![],
40                by_ref: if *by_ref {
41                    Some(token::Ref::default())
42                } else {
43                    None
44                },
45                mutability: if *is_mut {
46                    Some(token::Mut::default())
47                } else {
48                    None
49                },
50                ident: ident(name),
51                subpat: None,
52            })),
53            PurePattern::Wild => Ok(syn::Pat::Wild(syn::PatWild {
54                attrs: vec![],
55                underscore_token: token::Underscore::default(),
56            })),
57            PurePattern::Tuple(pats) => Ok(syn::Pat::Tuple(syn::PatTuple {
58                attrs: vec![],
59                paren_token: token::Paren::default(),
60                elems: pats.iter().map(|p| p.to_syn()).collect::<Result<_, _>>()?,
61            })),
62            PurePattern::Struct { path, fields, rest } => {
63                // Check if this is a tuple struct pattern (fields are numbered)
64                // Conditions for tuple struct:
65                // - Not a rest pattern (rest patterns use braces: `Foo { .. }`)
66                // - Has at least one field (empty fields means unit struct or rest-only)
67                // - All field names are numeric (positional)
68                let is_tuple_struct = !*rest
69                    && !fields.is_empty()
70                    && fields.iter().all(|(name, _)| name.parse::<u32>().is_ok());
71                if is_tuple_struct {
72                    Ok(syn::Pat::TupleStruct(syn::PatTupleStruct {
73                        attrs: vec![],
74                        qself: None,
75                        path: try_parse_path(path)?,
76                        paren_token: token::Paren::default(),
77                        elems: fields
78                            .iter()
79                            .map(|(_, pat)| pat.to_syn())
80                            .collect::<Result<_, _>>()?,
81                    }))
82                } else {
83                    // R10: emit field-name shorthand (`Foo { x }`) instead of
84                    // the redundant explicit form (`Foo { x: x }`) when the
85                    // binding pattern is a plain identifier whose name matches
86                    // the field name. The `redundant_field_names` clippy lint
87                    // fires on the explicit form, so unconditionally emitting
88                    // `colon_token: Some(...)` floods every downstream
89                    // round-trip with hundreds of warnings (observed by
90                    // `rl061_tempfile_fn_observe`). Shorthand is the Rust
91                    // idiomatic form and is semantically identical to the
92                    // explicit form for plain ident bindings.
93                    let syn_fields = fields
94                        .iter()
95                        .map(|(name, pat)| {
96                            // R10'': accept binding-mode-qualified
97                            // shorthand (`Foo { ref x }`, `Foo { mut x }`,
98                            // `Foo { ref mut x }`). Rust grammar allows the
99                            // shorthand form to carry `ref` / `mut` on the
100                            // binding itself, and syn's FieldPat emits
101                            // shorthand by inspecting the inner `Pat::Ident`'s
102                            // `by_ref` / `mutability` tokens when
103                            // `colon_token` is `None`. The original R10 fix
104                            // restricted shorthand to plain `Foo { x }`,
105                            // leaving `Foo { x: ref x }` / `Foo { x: mut x }`
106                            // explicit and triggering `redundant_field_names`
107                            // on every such pattern (observed in
108                            // `rl061_tempfile_fn_observe`).
109                            let is_shorthand = matches!(
110                                pat,
111                                PurePattern::Ident { name: pat_name, .. }
112                                    if pat_name == name
113                            );
114                            Ok(syn::FieldPat {
115                                attrs: vec![],
116                                member: syn::Member::Named(ident(name)),
117                                colon_token: if is_shorthand {
118                                    None
119                                } else {
120                                    Some(token::Colon::default())
121                                },
122                                pat: Box::new(pat.to_syn()?),
123                            })
124                        })
125                        .collect::<Result<_, ToSynError>>()?;
126                    Ok(syn::Pat::Struct(syn::PatStruct {
127                        attrs: vec![],
128                        qself: None,
129                        path: try_parse_path(path)?,
130                        brace_token: token::Brace::default(),
131                        fields: syn_fields,
132                        rest: if *rest {
133                            Some(syn::PatRest {
134                                attrs: vec![],
135                                dot2_token: token::DotDot::default(),
136                            })
137                        } else {
138                            None
139                        },
140                    }))
141                }
142            }
143            PurePattern::Ref { is_mut, pattern } => Ok(syn::Pat::Reference(syn::PatReference {
144                attrs: vec![],
145                and_token: token::And::default(),
146                mutability: if *is_mut {
147                    Some(token::Mut::default())
148                } else {
149                    None
150                },
151                pat: Box::new(pattern.to_syn()?),
152            })),
153            PurePattern::Lit(lit) => match syn::parse_str::<syn::Expr>(lit) {
154                Ok(syn::Expr::Lit(lit_expr)) => Ok(syn::Pat::Lit(lit_expr)),
155                Ok(_) => Err(ToSynError::ParsePattern {
156                    input: lit.clone(),
157                    message: "Expected literal pattern but got non-literal expression".to_string(),
158                }),
159                Err(e) => Err(ToSynError::ParsePattern {
160                    input: lit.clone(),
161                    message: e.to_string(),
162                }),
163            },
164            PurePattern::Or(pats) => Ok(syn::Pat::Or(syn::PatOr {
165                attrs: vec![],
166                leading_vert: None,
167                cases: pats.iter().map(|p| p.to_syn()).collect::<Result<_, _>>()?,
168            })),
169            PurePattern::Path(path) => {
170                // Debug assertion: PurePattern::Path should only contain simple paths
171                // Complex patterns (tuples, structs, rest patterns) should use PurePattern::Other
172                debug_assert!(
173                    !path.contains('(') && !path.contains('{') && !path.contains(".."),
174                    "PurePattern::Path received a complex pattern '{}'. Use PurePattern::Other instead.",
175                    path
176                );
177                Ok(syn::Pat::Path(syn::ExprPath {
178                    attrs: vec![],
179                    qself: None,
180                    path: try_parse_path(path)?,
181                }))
182            }
183            PurePattern::Range {
184                start,
185                end,
186                inclusive,
187            } => {
188                let start_expr = start
189                    .as_ref()
190                    .map(|s| {
191                        syn::parse_str::<syn::Expr>(s).map_err(|e| ToSynError::ParseExpr {
192                            input: s.clone(),
193                            message: e.to_string(),
194                        })
195                    })
196                    .transpose()?;
197                let end_expr = end
198                    .as_ref()
199                    .map(|s| {
200                        syn::parse_str::<syn::Expr>(s).map_err(|e| ToSynError::ParseExpr {
201                            input: s.clone(),
202                            message: e.to_string(),
203                        })
204                    })
205                    .transpose()?;
206                let limits = if *inclusive {
207                    syn::RangeLimits::Closed(token::DotDotEq::default())
208                } else {
209                    syn::RangeLimits::HalfOpen(token::DotDot::default())
210                };
211                Ok(syn::Pat::Range(syn::ExprRange {
212                    attrs: vec![],
213                    start: start_expr.map(Box::new),
214                    limits,
215                    end: end_expr.map(Box::new),
216                }))
217            }
218            PurePattern::Slice(pats) => Ok(syn::Pat::Slice(syn::PatSlice {
219                attrs: vec![],
220                bracket_token: token::Bracket::default(),
221                elems: pats.iter().map(|p| p.to_syn()).collect::<Result<_, _>>()?,
222            })),
223            PurePattern::Rest => Ok(syn::Pat::Rest(syn::PatRest {
224                attrs: vec![],
225                dot2_token: token::DotDot::default(),
226            })),
227            PurePattern::Other(s) => {
228                let tokens: proc_macro2::TokenStream =
229                    s.parse()
230                        .map_err(|e: proc_macro2::LexError| ToSynError::ParsePattern {
231                            input: s.clone(),
232                            message: format!("{}", e),
233                        })?;
234                Ok(syn::Pat::Verbatim(tokens))
235            }
236        }
237    }
238}
239
240impl ToSyn for PureExpr {
241    type Output = syn::Expr;
242
243    fn to_syn(&self) -> Result<syn::Expr, ToSynError> {
244        match self {
245            PureExpr::Lit(lit) => syn::parse_str(lit).map_err(|e| ToSynError::ParseExpr {
246                input: lit.clone(),
247                message: e.to_string(),
248            }),
249            PureExpr::Path(path) => Ok(syn::Expr::Path(syn::ExprPath {
250                attrs: vec![],
251                qself: None,
252                path: try_parse_path(path)?,
253            })),
254            PureExpr::Binary { op, left, right } => {
255                // Handle assignment specially - it's Expr::Assign, not Expr::Binary
256                if op == "=" {
257                    Ok(syn::Expr::Assign(syn::ExprAssign {
258                        attrs: vec![],
259                        left: Box::new(left.to_syn()?),
260                        eq_token: token::Eq::default(),
261                        right: Box::new(right.to_syn()?),
262                    }))
263                } else {
264                    Ok(syn::Expr::Binary(syn::ExprBinary {
265                        attrs: vec![],
266                        left: Box::new(left.to_syn()?),
267                        op: syn::parse_str(op).map_err(|e| ToSynError::ParseExpr {
268                            input: op.clone(),
269                            message: format!("Failed to parse binary operator: {}", e),
270                        })?,
271                        right: Box::new(right.to_syn()?),
272                    }))
273                }
274            }
275            PureExpr::Unary { op, expr } => Ok(syn::Expr::Unary(syn::ExprUnary {
276                attrs: vec![],
277                op: syn::parse_str(op).map_err(|e| ToSynError::ParseExpr {
278                    input: op.clone(),
279                    message: format!("Failed to parse unary operator: {}", e),
280                })?,
281                expr: Box::new(expr.to_syn()?),
282            })),
283            PureExpr::Call { func, args } => Ok(syn::Expr::Call(syn::ExprCall {
284                attrs: vec![],
285                func: Box::new(func.to_syn()?),
286                paren_token: token::Paren::default(),
287                args: args.iter().map(|a| a.to_syn()).collect::<Result<_, _>>()?,
288            })),
289            PureExpr::MethodCall {
290                receiver,
291                method,
292                turbofish,
293                args,
294            } => Ok(syn::Expr::MethodCall(syn::ExprMethodCall {
295                attrs: vec![],
296                receiver: Box::new(receiver.to_syn()?),
297                dot_token: token::Dot::default(),
298                method: ident(method),
299                turbofish: turbofish.as_ref().and_then(|t| {
300                    // Parse turbofish args like "T, U" into AngleBracketedGenericArguments
301                    let args_str = format!("::<{}>", t);
302                    syn::parse_str::<syn::AngleBracketedGenericArguments>(&args_str).ok()
303                }),
304                paren_token: token::Paren::default(),
305                args: args.iter().map(|a| a.to_syn()).collect::<Result<_, _>>()?,
306            })),
307            PureExpr::Field { expr, field } => Ok(syn::Expr::Field(syn::ExprField {
308                attrs: vec![],
309                base: Box::new(expr.to_syn()?),
310                dot_token: token::Dot::default(),
311                member: if let Ok(index) = field.parse::<u32>() {
312                    syn::Member::Unnamed(syn::Index {
313                        index,
314                        span: Span::call_site(),
315                    })
316                } else {
317                    syn::Member::Named(ident(field))
318                },
319            })),
320            PureExpr::Index { expr, index } => Ok(syn::Expr::Index(syn::ExprIndex {
321                attrs: vec![],
322                expr: Box::new(expr.to_syn()?),
323                bracket_token: token::Bracket::default(),
324                index: Box::new(index.to_syn()?),
325            })),
326            PureExpr::Block { label, block } => Ok(syn::Expr::Block(syn::ExprBlock {
327                attrs: vec![],
328                label: label.as_ref().map(|l| make_label(l)),
329                block: block.to_syn()?,
330            })),
331            PureExpr::If {
332                cond,
333                then_branch,
334                else_branch,
335            } => {
336                let else_b = else_branch
337                    .as_ref()
338                    .map(|e| Ok((token::Else::default(), Box::new(e.to_syn()?))))
339                    .transpose()?;
340                Ok(syn::Expr::If(syn::ExprIf {
341                    attrs: vec![],
342                    if_token: token::If::default(),
343                    cond: Box::new(cond.to_syn()?),
344                    then_branch: then_branch.to_syn()?,
345                    else_branch: else_b,
346                }))
347            }
348            PureExpr::Match { expr, arms } => Ok(syn::Expr::Match(syn::ExprMatch {
349                attrs: vec![],
350                match_token: token::Match::default(),
351                expr: Box::new(expr.to_syn()?),
352                brace_token: token::Brace::default(),
353                arms: arms
354                    .iter()
355                    .map(|a| a.to_syn())
356                    .collect::<Result<Vec<_>, _>>()?,
357            })),
358            PureExpr::Loop { label, body } => Ok(syn::Expr::Loop(syn::ExprLoop {
359                attrs: vec![],
360                label: label.as_ref().map(|l| make_label(l)),
361                loop_token: token::Loop::default(),
362                body: body.to_syn()?,
363            })),
364            PureExpr::While { label, cond, body } => Ok(syn::Expr::While(syn::ExprWhile {
365                attrs: vec![],
366                label: label.as_ref().map(|l| make_label(l)),
367                while_token: token::While::default(),
368                cond: Box::new(cond.to_syn()?),
369                body: body.to_syn()?,
370            })),
371            PureExpr::For {
372                label,
373                pat,
374                expr,
375                body,
376            } => Ok(syn::Expr::ForLoop(syn::ExprForLoop {
377                attrs: vec![],
378                label: label.as_ref().map(|l| make_label(l)),
379                for_token: token::For::default(),
380                pat: Box::new(pat.to_syn()?),
381                in_token: token::In::default(),
382                expr: Box::new(expr.to_syn()?),
383                body: body.to_syn()?,
384            })),
385            PureExpr::Return(expr) => {
386                let ret_expr = expr
387                    .as_ref()
388                    .map(|e| Ok(Box::new(e.to_syn()?)))
389                    .transpose()?;
390                Ok(syn::Expr::Return(syn::ExprReturn {
391                    attrs: vec![],
392                    return_token: token::Return::default(),
393                    expr: ret_expr,
394                }))
395            }
396            PureExpr::Break { label, expr } => {
397                let break_expr = expr
398                    .as_ref()
399                    .map(|e| Ok(Box::new(e.to_syn()?)))
400                    .transpose()?;
401                Ok(syn::Expr::Break(syn::ExprBreak {
402                    attrs: vec![],
403                    break_token: token::Break::default(),
404                    label: label.as_ref().map(|l| make_lifetime(l)),
405                    expr: break_expr,
406                }))
407            }
408            PureExpr::Continue { label } => Ok(syn::Expr::Continue(syn::ExprContinue {
409                attrs: vec![],
410                continue_token: token::Continue::default(),
411                label: label.as_ref().map(|l| make_lifetime(l)),
412            })),
413            PureExpr::Closure {
414                is_async,
415                is_move,
416                params,
417                ret,
418                body,
419            } => {
420                let inputs = params
421                    .iter()
422                    .map(|cp| {
423                        let pat = cp.pattern.to_syn()?;
424                        match &cp.ty {
425                            Some(ty) => Ok(syn::Pat::Type(syn::PatType {
426                                attrs: vec![],
427                                pat: Box::new(pat),
428                                colon_token: token::Colon::default(),
429                                ty: Box::new(ty.to_syn()?),
430                            })),
431                            None => Ok(pat),
432                        }
433                    })
434                    .collect::<Result<_, ToSynError>>()?;
435                let output = match ret {
436                    Some(ty) => {
437                        syn::ReturnType::Type(token::RArrow::default(), Box::new(ty.to_syn()?))
438                    }
439                    None => syn::ReturnType::Default,
440                };
441                Ok(syn::Expr::Closure(syn::ExprClosure {
442                    attrs: vec![],
443                    lifetimes: None,
444                    constness: None,
445                    movability: None,
446                    asyncness: if *is_async {
447                        Some(token::Async::default())
448                    } else {
449                        None
450                    },
451                    capture: if *is_move {
452                        Some(token::Move::default())
453                    } else {
454                        None
455                    },
456                    or1_token: token::Or::default(),
457                    inputs,
458                    or2_token: token::Or::default(),
459                    output,
460                    body: Box::new(body.to_syn()?),
461                }))
462            }
463            PureExpr::Struct { path, fields, rest } => {
464                // R10': emit field-init shorthand (`Foo { x }`) instead of
465                // the redundant explicit form (`Foo { x: x }`) when the
466                // initialiser is a bare path expression whose tail segment
467                // matches the field name. Sibling of the PurePattern::Struct
468                // shorthand fix (R10) — `redundant_field_names` clippy
469                // lint fires on both struct *literals* and struct *patterns*,
470                // and emitting the explicit form unconditionally produced
471                // hundreds of warnings post-round-trip.
472                let syn_fields = fields
473                    .iter()
474                    .map(|(name, expr)| {
475                        let is_shorthand = matches!(
476                            expr,
477                            PureExpr::Path(p) if p == name
478                        );
479                        Ok(syn::FieldValue {
480                            attrs: vec![],
481                            member: syn::Member::Named(ident(name)),
482                            colon_token: if is_shorthand {
483                                None
484                            } else {
485                                Some(token::Colon::default())
486                            },
487                            expr: expr.to_syn()?,
488                        })
489                    })
490                    .collect::<Result<_, ToSynError>>()?;
491                let (dot2_token, syn_rest) = match rest {
492                    Some(r) => (Some(token::DotDot::default()), Some(Box::new(r.to_syn()?))),
493                    None => (None, None),
494                };
495                Ok(syn::Expr::Struct(syn::ExprStruct {
496                    attrs: vec![],
497                    qself: None,
498                    path: try_parse_path(path)?,
499                    brace_token: token::Brace::default(),
500                    fields: syn_fields,
501                    dot2_token,
502                    rest: syn_rest,
503                }))
504            }
505            PureExpr::Tuple(elems) => Ok(syn::Expr::Tuple(syn::ExprTuple {
506                attrs: vec![],
507                paren_token: token::Paren::default(),
508                elems: elems.iter().map(|e| e.to_syn()).collect::<Result<_, _>>()?,
509            })),
510            PureExpr::Array(elems) => Ok(syn::Expr::Array(syn::ExprArray {
511                attrs: vec![],
512                bracket_token: token::Bracket::default(),
513                elems: elems.iter().map(|e| e.to_syn()).collect::<Result<_, _>>()?,
514            })),
515            PureExpr::Ref { is_mut, expr } => Ok(syn::Expr::Reference(syn::ExprReference {
516                attrs: vec![],
517                and_token: token::And::default(),
518                mutability: if *is_mut {
519                    Some(token::Mut::default())
520                } else {
521                    None
522                },
523                expr: Box::new(expr.to_syn()?),
524            })),
525            PureExpr::Macro {
526                name,
527                delimiter,
528                tokens,
529            } => {
530                let parsed_tokens: proc_macro2::TokenStream =
531                    tokens
532                        .parse()
533                        .map_err(|e: proc_macro2::LexError| ToSynError::Other {
534                            message: format!("Failed to parse macro tokens '{}': {}", tokens, e),
535                        })?;
536                Ok(syn::Expr::Macro(syn::ExprMacro {
537                    attrs: vec![],
538                    mac: syn::Macro {
539                        path: try_parse_path(name)?,
540                        bang_token: token::Not::default(),
541                        delimiter: match delimiter {
542                            MacroDelimiter::Paren => {
543                                syn::MacroDelimiter::Paren(token::Paren::default())
544                            }
545                            MacroDelimiter::Brace => {
546                                syn::MacroDelimiter::Brace(token::Brace::default())
547                            }
548                            MacroDelimiter::Bracket => {
549                                syn::MacroDelimiter::Bracket(token::Bracket::default())
550                            }
551                        },
552                        tokens: parsed_tokens,
553                    },
554                }))
555            }
556            PureExpr::Await(expr) => Ok(syn::Expr::Await(syn::ExprAwait {
557                attrs: vec![],
558                base: Box::new(expr.to_syn()?),
559                dot_token: token::Dot::default(),
560                await_token: token::Await::default(),
561            })),
562            PureExpr::Try(expr) => Ok(syn::Expr::Try(syn::ExprTry {
563                attrs: vec![],
564                expr: Box::new(expr.to_syn()?),
565                question_token: token::Question::default(),
566            })),
567            PureExpr::Range {
568                start,
569                end,
570                inclusive,
571            } => {
572                let start_expr = start
573                    .as_ref()
574                    .map(|e| Ok(Box::new(e.to_syn()?)))
575                    .transpose()?;
576                let end_expr = end
577                    .as_ref()
578                    .map(|e| Ok(Box::new(e.to_syn()?)))
579                    .transpose()?;
580                Ok(syn::Expr::Range(syn::ExprRange {
581                    attrs: vec![],
582                    start: start_expr,
583                    limits: if *inclusive {
584                        syn::RangeLimits::Closed(token::DotDotEq::default())
585                    } else {
586                        syn::RangeLimits::HalfOpen(token::DotDot::default())
587                    },
588                    end: end_expr,
589                }))
590            }
591            PureExpr::Cast { expr, ty } => Ok(syn::Expr::Cast(syn::ExprCast {
592                attrs: vec![],
593                expr: Box::new(expr.to_syn()?),
594                as_token: token::As::default(),
595                ty: Box::new(ty.to_syn()?),
596            })),
597            PureExpr::Let { pattern, expr } => Ok(syn::Expr::Let(syn::ExprLet {
598                attrs: vec![],
599                let_token: token::Let::default(),
600                pat: Box::new(pattern.to_syn()?),
601                eq_token: token::Eq::default(),
602                expr: Box::new(expr.to_syn()?),
603            })),
604            PureExpr::Async { is_move, body } => Ok(syn::Expr::Async(syn::ExprAsync {
605                attrs: vec![],
606                async_token: token::Async::default(),
607                capture: if *is_move {
608                    Some(token::Move::default())
609                } else {
610                    None
611                },
612                block: body.to_syn()?,
613            })),
614            PureExpr::Unsafe(body) => Ok(syn::Expr::Unsafe(syn::ExprUnsafe {
615                attrs: vec![],
616                unsafe_token: token::Unsafe::default(),
617                block: body.to_syn()?,
618            })),
619            PureExpr::Repeat { expr, len } => Ok(syn::Expr::Repeat(syn::ExprRepeat {
620                attrs: vec![],
621                bracket_token: token::Bracket::default(),
622                expr: Box::new(expr.to_syn()?),
623                semi_token: token::Semi::default(),
624                len: Box::new(len.to_syn()?),
625            })),
626            PureExpr::Other(s) => syn::parse_str(s).map_err(|e| ToSynError::ParseExpr {
627                input: s.clone(),
628                message: e.to_string(),
629            }),
630            PureExpr::Verbatim(raw) => {
631                // Register the raw bytes under a fresh per-thread index
632                // and emit a unique path-expr stub. PureFile::to_source
633                // runs the splice pass after prettyplease to replace
634                // the stub identifier with the raw bytes.
635                let idx = super::register_verbatim_expr(raw.clone());
636                let stub = format!("__ryo_verbatim_expr_{}", idx);
637                syn::parse_str::<syn::Expr>(&stub).map_err(|e| ToSynError::ParseExpr {
638                    input: stub,
639                    message: e.to_string(),
640                })
641            }
642        }
643    }
644}
645
646impl ToSyn for PureMatchArm {
647    type Output = syn::Arm;
648
649    fn to_syn(&self) -> Result<syn::Arm, ToSynError> {
650        let guard = self
651            .guard
652            .as_ref()
653            .map(|g| Ok((token::If::default(), Box::new(g.to_syn()?))))
654            .transpose()?;
655        Ok(syn::Arm {
656            attrs: vec![],
657            pat: self.pattern.to_syn()?,
658            guard,
659            fat_arrow_token: token::FatArrow::default(),
660            body: Box::new(self.body.to_syn()?),
661            comma: Some(token::Comma::default()),
662        })
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use crate::pure::ast::{PureBlock, PureClosureParam, PureStmt};
670    use crate::pure::convert::ToPure;
671    use quote::ToTokens;
672
673    #[test]
674    fn test_pure_pattern_ident() {
675        let pat = PurePattern::Ident {
676            name: "x".to_string(),
677            is_mut: false,
678            by_ref: false,
679        };
680        let syn_pat = pat.to_syn().unwrap();
681        let output = syn_pat.to_token_stream().to_string();
682        assert_eq!(output.trim(), "x");
683    }
684
685    #[test]
686    fn test_pure_pattern_mut_ident() {
687        let pat = PurePattern::Ident {
688            name: "x".to_string(),
689            is_mut: true,
690            by_ref: false,
691        };
692        let syn_pat = pat.to_syn().unwrap();
693        let output = syn_pat.to_token_stream().to_string();
694        assert!(output.contains("mut"), "Output: {}", output);
695    }
696
697    #[test]
698    fn test_pure_pattern_ref_ident_roundtrip() {
699        // Issue 083ec54c regression pin: `if let Some(ref x) = opt` round-trips
700        // without losing the `ref` binding qualifier. The mutation kind that
701        // originally surfaced this (RL001 unwrap → expect on bodies containing
702        // `if let Some(ref ...)`) goes through PureBlock → syn::Block, which
703        // descends into PureExpr::Let { pattern: PurePattern::Struct { path:
704        // "Some", fields: [("0", PurePattern::Ident { by_ref: true, .. })] } }.
705        // R5b/R10/R10'' built the structural carry path; this spec pins it.
706        use crate::pure::PureBlock;
707        use crate::pure::PureStmt;
708
709        // Parse from real source via syn → PureBlock to exercise both
710        // convert.rs (syn → Pure) and to_syn (Pure → syn) directions.
711        let src = "{ if let Some(ref query_file) = args.query_file { let _ = query_file; } }";
712        let syn_block: syn::Block = syn::parse_str(src).unwrap();
713        let pure_block: PureBlock = syn_block.to_pure();
714
715        // The PureBlock must carry `by_ref: true` on the inner Ident pattern.
716        let if_expr = match pure_block.stmts.first().unwrap() {
717            PureStmt::Expr(e) => e,
718            PureStmt::Semi(e) => e,
719            other => panic!("expected expr stmt, got {:?}", other),
720        };
721        let cond = match if_expr {
722            PureExpr::If { cond, .. } => cond.as_ref(),
723            other => panic!("expected If, got {:?}", other),
724        };
725        let let_pat = match cond {
726            PureExpr::Let { pattern, .. } => pattern,
727            other => panic!("expected Let, got {:?}", other),
728        };
729        let fields = match let_pat {
730            PurePattern::Struct { fields, path, .. } => {
731                assert_eq!(path, "Some");
732                fields
733            }
734            other => panic!("expected Struct pattern, got {:?}", other),
735        };
736        let inner = &fields[0].1;
737        match inner {
738            PurePattern::Ident {
739                name,
740                by_ref,
741                is_mut,
742            } => {
743                assert_eq!(name, "query_file");
744                assert!(*by_ref, "by_ref must survive syn → Pure conversion");
745                assert!(!*is_mut);
746            }
747            other => panic!("expected Ident, got {:?}", other),
748        }
749
750        // Round-trip back to syn and verify `ref` token survives in the
751        // generated source.
752        let regenerated = pure_block.to_syn().unwrap();
753        let out = quote::quote!(#regenerated).to_string();
754        assert!(
755            out.contains("ref query_file") || out.contains("ref  query_file"),
756            "regenerated source must contain `ref query_file`, got: {}",
757            out
758        );
759    }
760
761    #[test]
762    fn test_pure_pattern_ref_mut_ident_roundtrip() {
763        // Sibling: `ref mut x` binding qualifier must also survive the
764        // syn ↔ Pure round-trip. Covers the `mut` line in the original
765        // 083ec54c symptom ("mut パターン修飾子も検証が必要").
766        use crate::pure::PureBlock;
767
768        let src = "{ if let Some(ref mut buf) = sink { let _ = buf; } }";
769        let syn_block: syn::Block = syn::parse_str(src).unwrap();
770        let pure_block: PureBlock = syn_block.to_pure();
771        let regenerated = pure_block.to_syn().unwrap();
772        let out = quote::quote!(#regenerated).to_string();
773        assert!(
774            out.contains("ref mut buf"),
775            "regenerated source must contain `ref mut buf`, got: {}",
776            out
777        );
778    }
779
780    #[test]
781    fn test_pure_expr_binary() {
782        let expr = PureExpr::Binary {
783            op: "+".to_string(),
784            left: Box::new(PureExpr::Path("a".to_string())),
785            right: Box::new(PureExpr::Path("b".to_string())),
786        };
787        let syn_expr = expr.to_syn().unwrap();
788        let output = syn_expr.to_token_stream().to_string();
789        assert!(output.contains("+"), "Output: {}", output);
790    }
791
792    #[test]
793    fn test_pure_expr_closure() {
794        let expr = PureExpr::Closure {
795            is_async: false,
796            is_move: true,
797            params: vec![PureClosureParam::untyped(PurePattern::Ident {
798                name: "x".to_string(),
799                is_mut: false,
800                by_ref: false,
801            })],
802            ret: None,
803            body: Box::new(PureExpr::Path("x".to_string())),
804        };
805        let syn_expr = expr.to_syn().unwrap();
806        let output = syn_expr.to_token_stream().to_string();
807        assert!(output.contains("move"), "Output: {}", output);
808    }
809
810    #[test]
811    fn test_pure_match_arm() {
812        let arm = PureMatchArm {
813            pattern: PurePattern::Ident {
814                name: "x".to_string(),
815                is_mut: false,
816                by_ref: false,
817            },
818            guard: None,
819            body: PureExpr::Path("x".to_string()),
820        };
821        let syn_arm = arm.to_syn().unwrap();
822        let output = syn_arm.to_token_stream().to_string();
823        assert!(output.contains("=>"), "Output: {}", output);
824    }
825
826    #[test]
827    fn test_pure_expr_labeled_loop() {
828        let expr = PureExpr::Loop {
829            label: Some("outer".to_string()),
830            body: PureBlock { stmts: vec![] },
831        };
832        let syn_expr = expr.to_syn().unwrap();
833        let output = syn_expr.to_token_stream().to_string();
834        assert!(
835            output.contains("'outer") || output.contains("' outer"),
836            "Output should contain label: {}",
837            output
838        );
839        assert!(
840            output.contains("loop"),
841            "Output should contain 'loop': {}",
842            output
843        );
844    }
845
846    #[test]
847    fn test_pure_expr_labeled_while() {
848        let expr = PureExpr::While {
849            label: Some("my_loop".to_string()),
850            cond: Box::new(PureExpr::Lit("true".to_string())),
851            body: PureBlock { stmts: vec![] },
852        };
853        let syn_expr = expr.to_syn().unwrap();
854        let output = syn_expr.to_token_stream().to_string();
855        assert!(
856            output.contains("'my_loop") || output.contains("' my_loop"),
857            "Output should contain label: {}",
858            output
859        );
860        assert!(
861            output.contains("while"),
862            "Output should contain 'while': {}",
863            output
864        );
865    }
866
867    #[test]
868    fn test_pure_expr_labeled_for() {
869        let expr = PureExpr::For {
870            label: Some("iter_loop".to_string()),
871            pat: PurePattern::Ident {
872                name: "i".to_string(),
873                is_mut: false,
874                by_ref: false,
875            },
876            expr: Box::new(PureExpr::Path("items".to_string())),
877            body: PureBlock { stmts: vec![] },
878        };
879        let syn_expr = expr.to_syn().unwrap();
880        let output = syn_expr.to_token_stream().to_string();
881        assert!(
882            output.contains("'iter_loop") || output.contains("' iter_loop"),
883            "Output should contain label: {}",
884            output
885        );
886        assert!(
887            output.contains("for"),
888            "Output should contain 'for': {}",
889            output
890        );
891    }
892
893    #[test]
894    fn test_pure_expr_labeled_block() {
895        let expr = PureExpr::Block {
896            label: Some("my_block".to_string()),
897            block: PureBlock {
898                stmts: vec![PureStmt::Expr(PureExpr::Lit("42".to_string()))],
899            },
900        };
901        let syn_expr = expr.to_syn().unwrap();
902        let output = syn_expr.to_token_stream().to_string();
903        assert!(
904            output.contains("'my_block") || output.contains("' my_block"),
905            "Output should contain label: {}",
906            output
907        );
908    }
909
910    #[test]
911    fn test_pure_expr_break_with_label() {
912        let expr = PureExpr::Break {
913            label: Some("outer".to_string()),
914            expr: None,
915        };
916        let syn_expr = expr.to_syn().unwrap();
917        let output = syn_expr.to_token_stream().to_string();
918        assert!(
919            output.contains("break 'outer") || output.contains("break ' outer"),
920            "Output should contain 'break 'outer': {}",
921            output
922        );
923    }
924
925    #[test]
926    fn test_pure_expr_break_with_label_and_value() {
927        let expr = PureExpr::Break {
928            label: Some("block".to_string()),
929            expr: Some(Box::new(PureExpr::Lit("42".to_string()))),
930        };
931        let syn_expr = expr.to_syn().unwrap();
932        let output = syn_expr.to_token_stream().to_string();
933        assert!(
934            output.contains("break 'block 42") || output.contains("break ' block 42"),
935            "Output should contain 'break 'block 42': {}",
936            output
937        );
938    }
939
940    #[test]
941    fn test_pure_expr_continue_with_label() {
942        let expr = PureExpr::Continue {
943            label: Some("loop_label".to_string()),
944        };
945        let syn_expr = expr.to_syn().unwrap();
946        let output = syn_expr.to_token_stream().to_string();
947        assert!(
948            output.contains("continue 'loop_label") || output.contains("continue ' loop_label"),
949            "Output should contain 'continue 'loop_label': {}",
950            output
951        );
952    }
953
954    #[test]
955    fn test_pure_expr_continue_without_label() {
956        let expr = PureExpr::Continue { label: None };
957        let syn_expr = expr.to_syn().unwrap();
958        let output = syn_expr.to_token_stream().to_string();
959        assert_eq!(output.trim(), "continue");
960    }
961
962    #[test]
963    fn test_pure_expr_break_without_label() {
964        let expr = PureExpr::Break {
965            label: None,
966            expr: None,
967        };
968        let syn_expr = expr.to_syn().unwrap();
969        let output = syn_expr.to_token_stream().to_string();
970        assert_eq!(output.trim(), "break");
971    }
972
973    #[test]
974    fn test_pure_expr_loop_without_label() {
975        let expr = PureExpr::Loop {
976            label: None,
977            body: PureBlock { stmts: vec![] },
978        };
979        let syn_expr = expr.to_syn().unwrap();
980        let output = syn_expr.to_token_stream().to_string();
981        assert!(output.contains("loop"), "Output: {}", output);
982        assert!(
983            !output.contains("'"),
984            "Output should not contain label quote: {}",
985            output
986        );
987    }
988}