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 { name, is_mut } => Ok(syn::Pat::Ident(syn::PatIdent {
35                attrs: vec![],
36                by_ref: None,
37                mutability: if *is_mut {
38                    Some(token::Mut::default())
39                } else {
40                    None
41                },
42                ident: ident(name),
43                subpat: None,
44            })),
45            PurePattern::Wild => Ok(syn::Pat::Wild(syn::PatWild {
46                attrs: vec![],
47                underscore_token: token::Underscore::default(),
48            })),
49            PurePattern::Tuple(pats) => Ok(syn::Pat::Tuple(syn::PatTuple {
50                attrs: vec![],
51                paren_token: token::Paren::default(),
52                elems: pats.iter().map(|p| p.to_syn()).collect::<Result<_, _>>()?,
53            })),
54            PurePattern::Struct { path, fields, rest } => {
55                // Check if this is a tuple struct pattern (fields are numbered)
56                // Conditions for tuple struct:
57                // - Not a rest pattern (rest patterns use braces: `Foo { .. }`)
58                // - Has at least one field (empty fields means unit struct or rest-only)
59                // - All field names are numeric (positional)
60                let is_tuple_struct = !*rest
61                    && !fields.is_empty()
62                    && fields.iter().all(|(name, _)| name.parse::<u32>().is_ok());
63                if is_tuple_struct {
64                    Ok(syn::Pat::TupleStruct(syn::PatTupleStruct {
65                        attrs: vec![],
66                        qself: None,
67                        path: try_parse_path(path)?,
68                        paren_token: token::Paren::default(),
69                        elems: fields
70                            .iter()
71                            .map(|(_, pat)| pat.to_syn())
72                            .collect::<Result<_, _>>()?,
73                    }))
74                } else {
75                    let syn_fields = fields
76                        .iter()
77                        .map(|(name, pat)| {
78                            Ok(syn::FieldPat {
79                                attrs: vec![],
80                                member: syn::Member::Named(ident(name)),
81                                colon_token: Some(token::Colon::default()),
82                                pat: Box::new(pat.to_syn()?),
83                            })
84                        })
85                        .collect::<Result<_, ToSynError>>()?;
86                    Ok(syn::Pat::Struct(syn::PatStruct {
87                        attrs: vec![],
88                        qself: None,
89                        path: try_parse_path(path)?,
90                        brace_token: token::Brace::default(),
91                        fields: syn_fields,
92                        rest: if *rest {
93                            Some(syn::PatRest {
94                                attrs: vec![],
95                                dot2_token: token::DotDot::default(),
96                            })
97                        } else {
98                            None
99                        },
100                    }))
101                }
102            }
103            PurePattern::Ref { is_mut, pattern } => Ok(syn::Pat::Reference(syn::PatReference {
104                attrs: vec![],
105                and_token: token::And::default(),
106                mutability: if *is_mut {
107                    Some(token::Mut::default())
108                } else {
109                    None
110                },
111                pat: Box::new(pattern.to_syn()?),
112            })),
113            PurePattern::Lit(lit) => match syn::parse_str::<syn::Expr>(lit) {
114                Ok(syn::Expr::Lit(lit_expr)) => Ok(syn::Pat::Lit(lit_expr)),
115                Ok(_) => Err(ToSynError::ParsePattern {
116                    input: lit.clone(),
117                    message: "Expected literal pattern but got non-literal expression".to_string(),
118                }),
119                Err(e) => Err(ToSynError::ParsePattern {
120                    input: lit.clone(),
121                    message: e.to_string(),
122                }),
123            },
124            PurePattern::Or(pats) => Ok(syn::Pat::Or(syn::PatOr {
125                attrs: vec![],
126                leading_vert: None,
127                cases: pats.iter().map(|p| p.to_syn()).collect::<Result<_, _>>()?,
128            })),
129            PurePattern::Path(path) => {
130                // Debug assertion: PurePattern::Path should only contain simple paths
131                // Complex patterns (tuples, structs, rest patterns) should use PurePattern::Other
132                debug_assert!(
133                    !path.contains('(') && !path.contains('{') && !path.contains(".."),
134                    "PurePattern::Path received a complex pattern '{}'. Use PurePattern::Other instead.",
135                    path
136                );
137                Ok(syn::Pat::Path(syn::ExprPath {
138                    attrs: vec![],
139                    qself: None,
140                    path: try_parse_path(path)?,
141                }))
142            }
143            PurePattern::Range {
144                start,
145                end,
146                inclusive,
147            } => {
148                let start_expr = start
149                    .as_ref()
150                    .map(|s| {
151                        syn::parse_str::<syn::Expr>(s).map_err(|e| ToSynError::ParseExpr {
152                            input: s.clone(),
153                            message: e.to_string(),
154                        })
155                    })
156                    .transpose()?;
157                let end_expr = end
158                    .as_ref()
159                    .map(|s| {
160                        syn::parse_str::<syn::Expr>(s).map_err(|e| ToSynError::ParseExpr {
161                            input: s.clone(),
162                            message: e.to_string(),
163                        })
164                    })
165                    .transpose()?;
166                let limits = if *inclusive {
167                    syn::RangeLimits::Closed(token::DotDotEq::default())
168                } else {
169                    syn::RangeLimits::HalfOpen(token::DotDot::default())
170                };
171                Ok(syn::Pat::Range(syn::ExprRange {
172                    attrs: vec![],
173                    start: start_expr.map(Box::new),
174                    limits,
175                    end: end_expr.map(Box::new),
176                }))
177            }
178            PurePattern::Slice(pats) => Ok(syn::Pat::Slice(syn::PatSlice {
179                attrs: vec![],
180                bracket_token: token::Bracket::default(),
181                elems: pats.iter().map(|p| p.to_syn()).collect::<Result<_, _>>()?,
182            })),
183            PurePattern::Rest => Ok(syn::Pat::Rest(syn::PatRest {
184                attrs: vec![],
185                dot2_token: token::DotDot::default(),
186            })),
187            PurePattern::Other(s) => {
188                let tokens: proc_macro2::TokenStream =
189                    s.parse()
190                        .map_err(|e: proc_macro2::LexError| ToSynError::ParsePattern {
191                            input: s.clone(),
192                            message: format!("{}", e),
193                        })?;
194                Ok(syn::Pat::Verbatim(tokens))
195            }
196        }
197    }
198}
199
200impl ToSyn for PureExpr {
201    type Output = syn::Expr;
202
203    fn to_syn(&self) -> Result<syn::Expr, ToSynError> {
204        match self {
205            PureExpr::Lit(lit) => syn::parse_str(lit).map_err(|e| ToSynError::ParseExpr {
206                input: lit.clone(),
207                message: e.to_string(),
208            }),
209            PureExpr::Path(path) => Ok(syn::Expr::Path(syn::ExprPath {
210                attrs: vec![],
211                qself: None,
212                path: try_parse_path(path)?,
213            })),
214            PureExpr::Binary { op, left, right } => {
215                // Handle assignment specially - it's Expr::Assign, not Expr::Binary
216                if op == "=" {
217                    Ok(syn::Expr::Assign(syn::ExprAssign {
218                        attrs: vec![],
219                        left: Box::new(left.to_syn()?),
220                        eq_token: token::Eq::default(),
221                        right: Box::new(right.to_syn()?),
222                    }))
223                } else {
224                    Ok(syn::Expr::Binary(syn::ExprBinary {
225                        attrs: vec![],
226                        left: Box::new(left.to_syn()?),
227                        op: syn::parse_str(op).map_err(|e| ToSynError::ParseExpr {
228                            input: op.clone(),
229                            message: format!("Failed to parse binary operator: {}", e),
230                        })?,
231                        right: Box::new(right.to_syn()?),
232                    }))
233                }
234            }
235            PureExpr::Unary { op, expr } => Ok(syn::Expr::Unary(syn::ExprUnary {
236                attrs: vec![],
237                op: syn::parse_str(op).map_err(|e| ToSynError::ParseExpr {
238                    input: op.clone(),
239                    message: format!("Failed to parse unary operator: {}", e),
240                })?,
241                expr: Box::new(expr.to_syn()?),
242            })),
243            PureExpr::Call { func, args } => Ok(syn::Expr::Call(syn::ExprCall {
244                attrs: vec![],
245                func: Box::new(func.to_syn()?),
246                paren_token: token::Paren::default(),
247                args: args.iter().map(|a| a.to_syn()).collect::<Result<_, _>>()?,
248            })),
249            PureExpr::MethodCall {
250                receiver,
251                method,
252                turbofish,
253                args,
254            } => Ok(syn::Expr::MethodCall(syn::ExprMethodCall {
255                attrs: vec![],
256                receiver: Box::new(receiver.to_syn()?),
257                dot_token: token::Dot::default(),
258                method: ident(method),
259                turbofish: turbofish.as_ref().and_then(|t| {
260                    // Parse turbofish args like "T, U" into AngleBracketedGenericArguments
261                    let args_str = format!("::<{}>", t);
262                    syn::parse_str::<syn::AngleBracketedGenericArguments>(&args_str).ok()
263                }),
264                paren_token: token::Paren::default(),
265                args: args.iter().map(|a| a.to_syn()).collect::<Result<_, _>>()?,
266            })),
267            PureExpr::Field { expr, field } => Ok(syn::Expr::Field(syn::ExprField {
268                attrs: vec![],
269                base: Box::new(expr.to_syn()?),
270                dot_token: token::Dot::default(),
271                member: if let Ok(index) = field.parse::<u32>() {
272                    syn::Member::Unnamed(syn::Index {
273                        index,
274                        span: Span::call_site(),
275                    })
276                } else {
277                    syn::Member::Named(ident(field))
278                },
279            })),
280            PureExpr::Index { expr, index } => Ok(syn::Expr::Index(syn::ExprIndex {
281                attrs: vec![],
282                expr: Box::new(expr.to_syn()?),
283                bracket_token: token::Bracket::default(),
284                index: Box::new(index.to_syn()?),
285            })),
286            PureExpr::Block { label, block } => Ok(syn::Expr::Block(syn::ExprBlock {
287                attrs: vec![],
288                label: label.as_ref().map(|l| make_label(l)),
289                block: block.to_syn()?,
290            })),
291            PureExpr::If {
292                cond,
293                then_branch,
294                else_branch,
295            } => {
296                let else_b = else_branch
297                    .as_ref()
298                    .map(|e| Ok((token::Else::default(), Box::new(e.to_syn()?))))
299                    .transpose()?;
300                Ok(syn::Expr::If(syn::ExprIf {
301                    attrs: vec![],
302                    if_token: token::If::default(),
303                    cond: Box::new(cond.to_syn()?),
304                    then_branch: then_branch.to_syn()?,
305                    else_branch: else_b,
306                }))
307            }
308            PureExpr::Match { expr, arms } => Ok(syn::Expr::Match(syn::ExprMatch {
309                attrs: vec![],
310                match_token: token::Match::default(),
311                expr: Box::new(expr.to_syn()?),
312                brace_token: token::Brace::default(),
313                arms: arms
314                    .iter()
315                    .map(|a| a.to_syn())
316                    .collect::<Result<Vec<_>, _>>()?,
317            })),
318            PureExpr::Loop { label, body } => Ok(syn::Expr::Loop(syn::ExprLoop {
319                attrs: vec![],
320                label: label.as_ref().map(|l| make_label(l)),
321                loop_token: token::Loop::default(),
322                body: body.to_syn()?,
323            })),
324            PureExpr::While { label, cond, body } => Ok(syn::Expr::While(syn::ExprWhile {
325                attrs: vec![],
326                label: label.as_ref().map(|l| make_label(l)),
327                while_token: token::While::default(),
328                cond: Box::new(cond.to_syn()?),
329                body: body.to_syn()?,
330            })),
331            PureExpr::For {
332                label,
333                pat,
334                expr,
335                body,
336            } => Ok(syn::Expr::ForLoop(syn::ExprForLoop {
337                attrs: vec![],
338                label: label.as_ref().map(|l| make_label(l)),
339                for_token: token::For::default(),
340                pat: Box::new(pat.to_syn()?),
341                in_token: token::In::default(),
342                expr: Box::new(expr.to_syn()?),
343                body: body.to_syn()?,
344            })),
345            PureExpr::Return(expr) => {
346                let ret_expr = expr
347                    .as_ref()
348                    .map(|e| Ok(Box::new(e.to_syn()?)))
349                    .transpose()?;
350                Ok(syn::Expr::Return(syn::ExprReturn {
351                    attrs: vec![],
352                    return_token: token::Return::default(),
353                    expr: ret_expr,
354                }))
355            }
356            PureExpr::Break { label, expr } => {
357                let break_expr = expr
358                    .as_ref()
359                    .map(|e| Ok(Box::new(e.to_syn()?)))
360                    .transpose()?;
361                Ok(syn::Expr::Break(syn::ExprBreak {
362                    attrs: vec![],
363                    break_token: token::Break::default(),
364                    label: label.as_ref().map(|l| make_lifetime(l)),
365                    expr: break_expr,
366                }))
367            }
368            PureExpr::Continue { label } => Ok(syn::Expr::Continue(syn::ExprContinue {
369                attrs: vec![],
370                continue_token: token::Continue::default(),
371                label: label.as_ref().map(|l| make_lifetime(l)),
372            })),
373            PureExpr::Closure {
374                is_async,
375                is_move,
376                params,
377                ret,
378                body,
379            } => {
380                let inputs = params
381                    .iter()
382                    .map(|cp| {
383                        let pat = cp.pattern.to_syn()?;
384                        match &cp.ty {
385                            Some(ty) => Ok(syn::Pat::Type(syn::PatType {
386                                attrs: vec![],
387                                pat: Box::new(pat),
388                                colon_token: token::Colon::default(),
389                                ty: Box::new(ty.to_syn()?),
390                            })),
391                            None => Ok(pat),
392                        }
393                    })
394                    .collect::<Result<_, ToSynError>>()?;
395                let output = match ret {
396                    Some(ty) => {
397                        syn::ReturnType::Type(token::RArrow::default(), Box::new(ty.to_syn()?))
398                    }
399                    None => syn::ReturnType::Default,
400                };
401                Ok(syn::Expr::Closure(syn::ExprClosure {
402                    attrs: vec![],
403                    lifetimes: None,
404                    constness: None,
405                    movability: None,
406                    asyncness: if *is_async {
407                        Some(token::Async::default())
408                    } else {
409                        None
410                    },
411                    capture: if *is_move {
412                        Some(token::Move::default())
413                    } else {
414                        None
415                    },
416                    or1_token: token::Or::default(),
417                    inputs,
418                    or2_token: token::Or::default(),
419                    output,
420                    body: Box::new(body.to_syn()?),
421                }))
422            }
423            PureExpr::Struct { path, fields } => {
424                let syn_fields = fields
425                    .iter()
426                    .map(|(name, expr)| {
427                        Ok(syn::FieldValue {
428                            attrs: vec![],
429                            member: syn::Member::Named(ident(name)),
430                            colon_token: Some(token::Colon::default()),
431                            expr: expr.to_syn()?,
432                        })
433                    })
434                    .collect::<Result<_, ToSynError>>()?;
435                Ok(syn::Expr::Struct(syn::ExprStruct {
436                    attrs: vec![],
437                    qself: None,
438                    path: try_parse_path(path)?,
439                    brace_token: token::Brace::default(),
440                    fields: syn_fields,
441                    dot2_token: None,
442                    rest: None,
443                }))
444            }
445            PureExpr::Tuple(elems) => Ok(syn::Expr::Tuple(syn::ExprTuple {
446                attrs: vec![],
447                paren_token: token::Paren::default(),
448                elems: elems.iter().map(|e| e.to_syn()).collect::<Result<_, _>>()?,
449            })),
450            PureExpr::Array(elems) => Ok(syn::Expr::Array(syn::ExprArray {
451                attrs: vec![],
452                bracket_token: token::Bracket::default(),
453                elems: elems.iter().map(|e| e.to_syn()).collect::<Result<_, _>>()?,
454            })),
455            PureExpr::Ref { is_mut, expr } => Ok(syn::Expr::Reference(syn::ExprReference {
456                attrs: vec![],
457                and_token: token::And::default(),
458                mutability: if *is_mut {
459                    Some(token::Mut::default())
460                } else {
461                    None
462                },
463                expr: Box::new(expr.to_syn()?),
464            })),
465            PureExpr::Macro {
466                name,
467                delimiter,
468                tokens,
469            } => {
470                let parsed_tokens: proc_macro2::TokenStream =
471                    tokens
472                        .parse()
473                        .map_err(|e: proc_macro2::LexError| ToSynError::Other {
474                            message: format!("Failed to parse macro tokens '{}': {}", tokens, e),
475                        })?;
476                Ok(syn::Expr::Macro(syn::ExprMacro {
477                    attrs: vec![],
478                    mac: syn::Macro {
479                        path: try_parse_path(name)?,
480                        bang_token: token::Not::default(),
481                        delimiter: match delimiter {
482                            MacroDelimiter::Paren => {
483                                syn::MacroDelimiter::Paren(token::Paren::default())
484                            }
485                            MacroDelimiter::Brace => {
486                                syn::MacroDelimiter::Brace(token::Brace::default())
487                            }
488                            MacroDelimiter::Bracket => {
489                                syn::MacroDelimiter::Bracket(token::Bracket::default())
490                            }
491                        },
492                        tokens: parsed_tokens,
493                    },
494                }))
495            }
496            PureExpr::Await(expr) => Ok(syn::Expr::Await(syn::ExprAwait {
497                attrs: vec![],
498                base: Box::new(expr.to_syn()?),
499                dot_token: token::Dot::default(),
500                await_token: token::Await::default(),
501            })),
502            PureExpr::Try(expr) => Ok(syn::Expr::Try(syn::ExprTry {
503                attrs: vec![],
504                expr: Box::new(expr.to_syn()?),
505                question_token: token::Question::default(),
506            })),
507            PureExpr::Range {
508                start,
509                end,
510                inclusive,
511            } => {
512                let start_expr = start
513                    .as_ref()
514                    .map(|e| Ok(Box::new(e.to_syn()?)))
515                    .transpose()?;
516                let end_expr = end
517                    .as_ref()
518                    .map(|e| Ok(Box::new(e.to_syn()?)))
519                    .transpose()?;
520                Ok(syn::Expr::Range(syn::ExprRange {
521                    attrs: vec![],
522                    start: start_expr,
523                    limits: if *inclusive {
524                        syn::RangeLimits::Closed(token::DotDotEq::default())
525                    } else {
526                        syn::RangeLimits::HalfOpen(token::DotDot::default())
527                    },
528                    end: end_expr,
529                }))
530            }
531            PureExpr::Cast { expr, ty } => Ok(syn::Expr::Cast(syn::ExprCast {
532                attrs: vec![],
533                expr: Box::new(expr.to_syn()?),
534                as_token: token::As::default(),
535                ty: Box::new(ty.to_syn()?),
536            })),
537            PureExpr::Let { pattern, expr } => Ok(syn::Expr::Let(syn::ExprLet {
538                attrs: vec![],
539                let_token: token::Let::default(),
540                pat: Box::new(pattern.to_syn()?),
541                eq_token: token::Eq::default(),
542                expr: Box::new(expr.to_syn()?),
543            })),
544            PureExpr::Async { is_move, body } => Ok(syn::Expr::Async(syn::ExprAsync {
545                attrs: vec![],
546                async_token: token::Async::default(),
547                capture: if *is_move {
548                    Some(token::Move::default())
549                } else {
550                    None
551                },
552                block: body.to_syn()?,
553            })),
554            PureExpr::Unsafe(body) => Ok(syn::Expr::Unsafe(syn::ExprUnsafe {
555                attrs: vec![],
556                unsafe_token: token::Unsafe::default(),
557                block: body.to_syn()?,
558            })),
559            PureExpr::Repeat { expr, len } => Ok(syn::Expr::Repeat(syn::ExprRepeat {
560                attrs: vec![],
561                bracket_token: token::Bracket::default(),
562                expr: Box::new(expr.to_syn()?),
563                semi_token: token::Semi::default(),
564                len: Box::new(len.to_syn()?),
565            })),
566            PureExpr::Other(s) => syn::parse_str(s).map_err(|e| ToSynError::ParseExpr {
567                input: s.clone(),
568                message: e.to_string(),
569            }),
570        }
571    }
572}
573
574impl ToSyn for PureMatchArm {
575    type Output = syn::Arm;
576
577    fn to_syn(&self) -> Result<syn::Arm, ToSynError> {
578        let guard = self
579            .guard
580            .as_ref()
581            .map(|g| Ok((token::If::default(), Box::new(g.to_syn()?))))
582            .transpose()?;
583        Ok(syn::Arm {
584            attrs: vec![],
585            pat: self.pattern.to_syn()?,
586            guard,
587            fat_arrow_token: token::FatArrow::default(),
588            body: Box::new(self.body.to_syn()?),
589            comma: Some(token::Comma::default()),
590        })
591    }
592}
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597    use crate::pure::ast::{PureBlock, PureClosureParam, PureStmt};
598    use quote::ToTokens;
599
600    #[test]
601    fn test_pure_pattern_ident() {
602        let pat = PurePattern::Ident {
603            name: "x".to_string(),
604            is_mut: false,
605        };
606        let syn_pat = pat.to_syn().unwrap();
607        let output = syn_pat.to_token_stream().to_string();
608        assert_eq!(output.trim(), "x");
609    }
610
611    #[test]
612    fn test_pure_pattern_mut_ident() {
613        let pat = PurePattern::Ident {
614            name: "x".to_string(),
615            is_mut: true,
616        };
617        let syn_pat = pat.to_syn().unwrap();
618        let output = syn_pat.to_token_stream().to_string();
619        assert!(output.contains("mut"), "Output: {}", output);
620    }
621
622    #[test]
623    fn test_pure_expr_binary() {
624        let expr = PureExpr::Binary {
625            op: "+".to_string(),
626            left: Box::new(PureExpr::Path("a".to_string())),
627            right: Box::new(PureExpr::Path("b".to_string())),
628        };
629        let syn_expr = expr.to_syn().unwrap();
630        let output = syn_expr.to_token_stream().to_string();
631        assert!(output.contains("+"), "Output: {}", output);
632    }
633
634    #[test]
635    fn test_pure_expr_closure() {
636        let expr = PureExpr::Closure {
637            is_async: false,
638            is_move: true,
639            params: vec![PureClosureParam::untyped(PurePattern::Ident {
640                name: "x".to_string(),
641                is_mut: false,
642            })],
643            ret: None,
644            body: Box::new(PureExpr::Path("x".to_string())),
645        };
646        let syn_expr = expr.to_syn().unwrap();
647        let output = syn_expr.to_token_stream().to_string();
648        assert!(output.contains("move"), "Output: {}", output);
649    }
650
651    #[test]
652    fn test_pure_match_arm() {
653        let arm = PureMatchArm {
654            pattern: PurePattern::Ident {
655                name: "x".to_string(),
656                is_mut: false,
657            },
658            guard: None,
659            body: PureExpr::Path("x".to_string()),
660        };
661        let syn_arm = arm.to_syn().unwrap();
662        let output = syn_arm.to_token_stream().to_string();
663        assert!(output.contains("=>"), "Output: {}", output);
664    }
665
666    #[test]
667    fn test_pure_expr_labeled_loop() {
668        let expr = PureExpr::Loop {
669            label: Some("outer".to_string()),
670            body: PureBlock { stmts: vec![] },
671        };
672        let syn_expr = expr.to_syn().unwrap();
673        let output = syn_expr.to_token_stream().to_string();
674        assert!(
675            output.contains("'outer") || output.contains("' outer"),
676            "Output should contain label: {}",
677            output
678        );
679        assert!(
680            output.contains("loop"),
681            "Output should contain 'loop': {}",
682            output
683        );
684    }
685
686    #[test]
687    fn test_pure_expr_labeled_while() {
688        let expr = PureExpr::While {
689            label: Some("my_loop".to_string()),
690            cond: Box::new(PureExpr::Lit("true".to_string())),
691            body: PureBlock { stmts: vec![] },
692        };
693        let syn_expr = expr.to_syn().unwrap();
694        let output = syn_expr.to_token_stream().to_string();
695        assert!(
696            output.contains("'my_loop") || output.contains("' my_loop"),
697            "Output should contain label: {}",
698            output
699        );
700        assert!(
701            output.contains("while"),
702            "Output should contain 'while': {}",
703            output
704        );
705    }
706
707    #[test]
708    fn test_pure_expr_labeled_for() {
709        let expr = PureExpr::For {
710            label: Some("iter_loop".to_string()),
711            pat: PurePattern::Ident {
712                name: "i".to_string(),
713                is_mut: false,
714            },
715            expr: Box::new(PureExpr::Path("items".to_string())),
716            body: PureBlock { stmts: vec![] },
717        };
718        let syn_expr = expr.to_syn().unwrap();
719        let output = syn_expr.to_token_stream().to_string();
720        assert!(
721            output.contains("'iter_loop") || output.contains("' iter_loop"),
722            "Output should contain label: {}",
723            output
724        );
725        assert!(
726            output.contains("for"),
727            "Output should contain 'for': {}",
728            output
729        );
730    }
731
732    #[test]
733    fn test_pure_expr_labeled_block() {
734        let expr = PureExpr::Block {
735            label: Some("my_block".to_string()),
736            block: PureBlock {
737                stmts: vec![PureStmt::Expr(PureExpr::Lit("42".to_string()))],
738            },
739        };
740        let syn_expr = expr.to_syn().unwrap();
741        let output = syn_expr.to_token_stream().to_string();
742        assert!(
743            output.contains("'my_block") || output.contains("' my_block"),
744            "Output should contain label: {}",
745            output
746        );
747    }
748
749    #[test]
750    fn test_pure_expr_break_with_label() {
751        let expr = PureExpr::Break {
752            label: Some("outer".to_string()),
753            expr: None,
754        };
755        let syn_expr = expr.to_syn().unwrap();
756        let output = syn_expr.to_token_stream().to_string();
757        assert!(
758            output.contains("break 'outer") || output.contains("break ' outer"),
759            "Output should contain 'break 'outer': {}",
760            output
761        );
762    }
763
764    #[test]
765    fn test_pure_expr_break_with_label_and_value() {
766        let expr = PureExpr::Break {
767            label: Some("block".to_string()),
768            expr: Some(Box::new(PureExpr::Lit("42".to_string()))),
769        };
770        let syn_expr = expr.to_syn().unwrap();
771        let output = syn_expr.to_token_stream().to_string();
772        assert!(
773            output.contains("break 'block 42") || output.contains("break ' block 42"),
774            "Output should contain 'break 'block 42': {}",
775            output
776        );
777    }
778
779    #[test]
780    fn test_pure_expr_continue_with_label() {
781        let expr = PureExpr::Continue {
782            label: Some("loop_label".to_string()),
783        };
784        let syn_expr = expr.to_syn().unwrap();
785        let output = syn_expr.to_token_stream().to_string();
786        assert!(
787            output.contains("continue 'loop_label") || output.contains("continue ' loop_label"),
788            "Output should contain 'continue 'loop_label': {}",
789            output
790        );
791    }
792
793    #[test]
794    fn test_pure_expr_continue_without_label() {
795        let expr = PureExpr::Continue { label: None };
796        let syn_expr = expr.to_syn().unwrap();
797        let output = syn_expr.to_token_stream().to_string();
798        assert_eq!(output.trim(), "continue");
799    }
800
801    #[test]
802    fn test_pure_expr_break_without_label() {
803        let expr = PureExpr::Break {
804            label: None,
805            expr: None,
806        };
807        let syn_expr = expr.to_syn().unwrap();
808        let output = syn_expr.to_token_stream().to_string();
809        assert_eq!(output.trim(), "break");
810    }
811
812    #[test]
813    fn test_pure_expr_loop_without_label() {
814        let expr = PureExpr::Loop {
815            label: None,
816            body: PureBlock { stmts: vec![] },
817        };
818        let syn_expr = expr.to_syn().unwrap();
819        let output = syn_expr.to_token_stream().to_string();
820        assert!(output.contains("loop"), "Output: {}", output);
821        assert!(
822            !output.contains("'"),
823            "Output should not contain label quote: {}",
824            output
825        );
826    }
827}